diff --git a/.github/agents/agentic-workflows.md b/.github/agents/agentic-workflows.md index 9a2e0130e8..b824e60580 100644 --- a/.github/agents/agentic-workflows.md +++ b/.github/agents/agentic-workflows.md @@ -35,7 +35,7 @@ Workflows may optionally include: - Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` - Workflow lock files: `.github/workflows/*.lock.yml` - Shared components: `.github/workflows/shared/*.md` -- Configuration: `.github/aw/github-agentic-workflows.md` +- Configuration: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` ## Problems This Solves @@ -54,10 +54,12 @@ When you interact with this agent, it will: ## Available Prompts +> **Note**: The prompt and reference files listed below are located in the [`github/gh-aw`](https://github.com/github/gh-aw) repository and are **not available locally** in this repository. Load them from their public URLs. + ### Create New Workflow **Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet -**Prompt file**: `.github/aw/create-agentic-workflow.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-agentic-workflow.md` **Use cases**: - "Create a workflow that triages issues" @@ -67,7 +69,7 @@ When you interact with this agent, it will: ### Update Existing Workflow **Load when**: User wants to modify, improve, or refactor an existing workflow -**Prompt file**: `.github/aw/update-agentic-workflow.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/update-agentic-workflow.md` **Use cases**: - "Add web-fetch tool to the issue-classifier workflow" @@ -77,7 +79,7 @@ When you interact with this agent, it will: ### Debug Workflow **Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors -**Prompt file**: `.github/aw/debug-agentic-workflow.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/debug-agentic-workflow.md` **Use cases**: - "Why is this workflow failing?" @@ -87,7 +89,7 @@ When you interact with this agent, it will: ### Upgrade Agentic Workflows **Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations -**Prompt file**: `.github/aw/upgrade-agentic-workflows.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/upgrade-agentic-workflows.md` **Use cases**: - "Upgrade all workflows to the latest version" @@ -97,7 +99,7 @@ When you interact with this agent, it will: ### Create a Report-Generating Workflow **Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment -**Prompt file**: `.github/aw/report.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/report.md` **Use cases**: - "Create a weekly CI health report" @@ -107,7 +109,7 @@ When you interact with this agent, it will: ### Create Shared Agentic Workflow **Load when**: User wants to create a reusable workflow component or wrap an MCP server -**Prompt file**: `.github/aw/create-shared-agentic-workflow.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-shared-agentic-workflow.md` **Use cases**: - "Create a shared component for Notion integration" @@ -117,7 +119,7 @@ When you interact with this agent, it will: ### Fix Dependabot PRs **Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) -**Prompt file**: `.github/aw/dependabot.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/dependabot.md` **Use cases**: - "Fix the open Dependabot PRs for npm dependencies" @@ -127,7 +129,7 @@ When you interact with this agent, it will: ### Analyze Test Coverage **Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. -**Prompt file**: `.github/aw/test-coverage.md` +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/test-coverage.md` **Use cases**: - "Create a workflow that comments coverage on PRs" @@ -137,7 +139,7 @@ When you interact with this agent, it will: ### CLI Commands Reference **Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access. -**Reference file**: `.github/aw/cli-commands.md` +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md` **Use cases**: - "How do I trigger workflow X on the main branch?" @@ -148,7 +150,7 @@ When you interact with this agent, it will: ### Token Consumption Optimization **Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes. -**Reference file**: `.github/aw/token-optimization.md` +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/token-optimization.md` **Use cases**: - "How do I reduce the token cost of this workflow?" @@ -161,7 +163,7 @@ When you interact with this agent, it will: ### Workflow Pattern Selection **Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows. -**Reference file**: `.github/aw/patterns.md` +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/patterns.md` **Use cases**: - "Which pattern should I use for multi-repo rollout?" @@ -174,7 +176,7 @@ When you interact with this agent, it will: When a user interacts with you: 1. **Identify the task type** from the user's request -2. **Load the appropriate prompt** from the repository paths listed above +2. **Load the appropriate prompt** from the URLs listed above 3. **Follow the loaded prompt's instructions** exactly 4. **If uncertain**, ask clarifying questions to determine the right prompt @@ -213,12 +215,12 @@ gh aw compile --validate ## Important Notes -- Always reference the instructions file at `.github/aw/github-agentic-workflows.md` for complete documentation +- Always reference the instructions file at `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` for complete documentation - Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud - Workflows must be compiled to `.lock.yml` files before running in GitHub Actions - **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF - Follow security best practices: minimal permissions, explicit network access, no template injection -- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See `.github/aw/network.md` for the full list of valid ecosystem identifiers and domain patterns. +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/network.md` for the full list of valid ecosystem identifiers and domain patterns. - **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. - **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand — not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch. -- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see `.github/aw/cli-commands.md` +- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c00c619c4f..e46e951c51 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,8 @@ updates: - directory: / ignore: - - dependency-name: "github/gh-aw-actions/**" # Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump. + - dependency-name: "github/gh-aw-actions/**" + - 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 diff --git a/.github/mcp.json b/.github/mcp.json index b953af2639..341a8b8758 100644 --- a/.github/mcp.json +++ b/.github/mcp.json @@ -1,10 +1,19 @@ { "mcpServers": { "github-agentic-workflows": { + "type": "local", "command": "gh", "args": [ "aw", "mcp-server" + ], + "tools": [ + "compile", + "audit", + "logs", + "inspect", + "status", + "audit-diff" ] } } diff --git a/.github/skills/agentic-workflow-designer/SKILL.md b/.github/skills/agentic-workflow-designer/SKILL.md new file mode 100644 index 0000000000..eadac2bf65 --- /dev/null +++ b/.github/skills/agentic-workflow-designer/SKILL.md @@ -0,0 +1,372 @@ +--- +name: agentic-workflow-designer +description: Conversational skill that interviews users to design new agentic workflows +disable-model-invocation: true +--- + +# Workflow Designer + +Use this skill to run a structured interview with users who know their goal but not the workflow syntax yet, then generate one complete workflow `.md` file. + +## When to Use This Skill + +Use this before `.github/aw/create-agentic-workflow.md` when requirements are unclear or incomplete. + +- Use `skills/agentic-workflow-designer/SKILL.md` to discover and confirm requirements. +- Use `.github/aw/create-agentic-workflow.md` once requirements are clear and ready for implementation. +- Use `.github/aw/agentic-chat.md` when the user wants a specification/pseudo-code instead of a runnable workflow file. + +## Interview Framework + +Ask one question at a time. Move to the next phase only after the current phase is clear. + +### Phase 1: Goal + +Ask: **"What do you want to automate?"** + +Capture: +- Workflow name (kebab-case candidate) +- Brief description +- Optional emoji + +### Phase 2: Trigger + +Ask: **"When should this run?"** + +Follow up only if needed: +- Which event type(s)? +- Any filters (labels, branches, commands)? +- Scheduled cadence (daily/weekly/hourly)? + +Map to the `on:` block. + +### Phase 3: Scope (Read/Write) + +Ask: +- **"What should it read?"** (issues, PRs, code, discussions, CI data) +- **"What should it create or update?"** (comments, issues, PRs, labels) + +Map to: +- `permissions:` (keep read-only for agent job) +- `tools:` +- `safe-outputs:` + +### Phase 4: Data Strategy + +Ask: +- **"What data does the agent need to make decisions?"** +- Follow up: **"Can we pre-fetch and aggregate that data with shell commands so the agent only reads compact JSON?"** + +Capture: +- Whether `steps:` should pre-fetch GitHub data with `gh` + `jq` +- Output paths under `/tmp/gh-aw/data/` +- Whether batch work should use sub-agents + +Map to: +- `steps:` +- Prompt references to pre-computed file paths + +### Phase 5: Guardrails + +Ask: **"Should it block merging, just advise, or silently log?"** + +Capture: +- Visibility expectations (comment, issue, no visible output) +- No-op behavior expectation + +Guide toward safe output behavior and explicit `noop` instructions. + +### Phase 6: Context & Network + +Ask: **"Does it need external APIs, web access, package installs, or MCP servers?"** + +Follow up: +- **"Any third-party services or MCP servers to include (for example Slack, Jira, Datadog, custom internal MCP)?"** +- **"Are you deploying on GitHub.com, GHEC with custom endpoints, or GHES?"** +- For each integration, identify required auth from source docs and map it to GitHub Actions secrets + workflow env variables. +- Ask for exact external domains (FQDN/wildcard). + +Map to: +- `network.allowed` +- Optional MCP/GitHub tool usage in `tools:` +- `secrets:` / `env:` wiring for integration tokens +- GHES/GHEC settings such as `engine.api-target` and `aw.json` `ghes: true` (when applicable) + +### Phase 7: Engine (optional) + +Ask only if ambiguous: **"Any AI engine preference?"** + +If no preference, suggest default: +- "I'd suggest Copilot since you haven't mentioned a preference. Sound good?" + +Map to `engine:` only when not default. + +### Phase 8: Confirmation + +Present a structured summary and ask for approval before generation. + +## Decision Heuristics + +### Trigger Mapping + +| User says... | Maps to | +|---|---| +| "when someone opens a PR" | `on: pull_request:` with `types: [opened]` | +| "when a PR is updated" | `on: pull_request:` with `types: [opened, synchronize]` | +| "every morning", "daily" | fuzzy schedule shorthand `on: schedule: daily on weekdays` (compiler expands to cron) | +| "every Monday", "weekly" | fuzzy schedule shorthand `on: schedule: weekly` (compiler expands to cron) | +| "when I say /review" | `on: slash_command:` with `name: review` (or requested command) | +| "when an issue is labeled bug" | `on: issues:` with `types: [labeled]` and label filter guidance | +| "run when label ai-review is added" | `on: label_command:` with `name`/`names`, optional event scoping, and label-as-command semantics | +| "run on PRs from forks" | `on: pull_request:` plus explicit `forks:` allowlist and fork security guardrails | +| "sometimes automatic, sometimes manual" | semi-active pattern: combine `schedule`/event triggers with `workflow_dispatch` | +| "manually", "on demand" | `on: workflow_dispatch:` | +| "when a deployment fails" | `on: deployment_status:` | +| "when another workflow finishes" | `on: workflow_run:` | + +### Safe Output Mapping + +| User says... | Maps to | +|---|---| +| "post a comment" | `add-comment` | +| "create an issue" | `create-issue` | +| "update issue title/body" | `update-issue` | +| "close the issue" | `close-issue` | +| "assign someone", "remove assignment" | `assign-to-user`, `unassign-from-user` | +| "set issue type/field/milestone" | `set-issue-type`, `set-issue-field`, `assign-milestone` | +| "open a PR", "submit changes" | `create-pull-request` | +| "update PR description/title" | `update-pull-request` | +| "close the PR", "merge the PR" | `close-pull-request`, `merge-pull-request` | +| "mark PR ready", "sync PR branch" | `mark-pull-request-as-ready-for-review`, `update-branch` | +| "commit a fix to the PR branch" | `push-to-pull-request-branch` | +| "approve / request changes" | `submit-pull-request-review` | +| "inline review comment", "reply to review thread" | `create-pull-request-review-comment`, `reply-to-pull-request-review-comment`, `resolve-pull-request-review-thread` | +| "start or edit discussion", "close discussion" | `create-discussion`, `update-discussion`, `close-discussion` | +| "request reviewer", "hide comment" | `add-reviewer`, `hide-comment` | +| "create/update project", "project status update" | `create-project`, `update-project`, `create-project-status-update` | +| "update release", "upload release asset" | `update-release`, `upload-asset` | +| "create/auto-fix code scan alert" | `create-code-scanning-alert`, `autofix-code-scanning-alert` | +| "start an agent session", "assign to an agent" | `create-agent-session`, `assign-to-agent` | +| "store persistent memory comment" | `comment-memory` | +| "link a sub-issue" | `link-sub-issue` | +| "add labels", "remove labels" | `add-labels`, `remove-labels` | +| "nothing visible", "just analyze" | no safe outputs required | + +### Network Mapping + +| User says... | Maps to | +|---|---| +| "calls an external API" | ask for exact FQDN/wildcard, then add to `network.allowed` | +| "reads GitHub data / clones repos" | include `github` in `network.allowed` | +| "uses GitHub Actions artifacts or cache" | include `github-actions` in `network.allowed` | +| "installs npm packages" | include `node` in `network.allowed` | +| "runs pip install" | include `python` in `network.allowed` | +| "builds Go code" | include `go` in `network.allowed` | +| "installs gems / uses Bundler" | include `ruby` in `network.allowed` | +| "runs cargo build" | include `rust` in `network.allowed` | +| "uses NuGet / .NET restore" | include `dotnet` in `network.allowed` | +| "builds with Maven / Gradle" | include `java` in `network.allowed` | +| "uses Docker / pulls container images / pushes to GHCR" | include `containers` in `network.allowed` | +| "runs Playwright browser tests" | include `playwright` in `network.allowed` | +| "runs apt install / yum / apk" | include `linux-distros` in `network.allowed` | +| "uses Terraform / HashiCorp registry" | include `terraform` in `network.allowed` | +| "connects to localhost / loopback / local services" | include `local` in `network.allowed` | +| "uses Swift Package Manager" | include `swift` in `network.allowed` | +| "uses Composer / PHP packages" | include `php` in `network.allowed` | +| "uses pub.dev / Dart packages" | include `dart` in `network.allowed` | +| "uses Hackage / Haskell packages" | include `haskell` in `network.allowed` | +| "uses CPAN / Perl packages" | include `perl` in `network.allowed` | +| "serves or loads web fonts" | include `fonts` in `network.allowed` | +| "uses Deno or JSR packages" | include `deno` in `network.allowed` | +| "uses Elixir / Hex packages" | include `elixir` in `network.allowed` | +| "uses Bazel build" | include `bazel` in `network.allowed` | +| "uses R / CRAN packages" | include `r` in `network.allowed` | +| "no external access" | `network.allowed: [defaults]` (or `[]` if explicitly zero network) | + +### Tool Mapping + +| User says... | Maps to | +|---|---| +| "read GitHub issues/PRs/workflows" | `tools.github` with `mode: gh-proxy` and minimal `toolsets` | +| "use full MCP server/tool definitions" | `tools.github` with `mode: local` | +| "use other MCP servers but keep token cost down" | `tools.cli-proxy: true` (hybrid CLI-proxy mode) | +| "edit files" | `edit` tool (default unless restricted) | +| "run commands/tests" | `bash` tool (default unless restricted) | +| "browse web pages/docs" | `web-fetch` and/or `web-search` | +| "test UI flows" | `playwright` | + +### Pattern Heuristics + +| User says... | Recommended named pattern | +|---|---| +| "triage issues automatically" | `IssueOps` | +| "run on /commands with human approval loops" | `ChatOps` | +| "run every weekday and keep improving" | `DailyOps` | +| "monitor workflow failures and trends" | `MonitorOps` | +| "process a big backlog in chunks" | `BatchOps` | +| "run manually with input parameters" | `DispatchOps` | +| "apply a label-based workflow" | `LabelOps` | +| "operate across multiple repositories" | `MultiRepoOps` | +| "coordinate multiple sub-agents" | `Orchestration` | +| "manage project board items" | `ProjectOps` | +| "research, plan, and assign issues" | `ResearchPlanAssignOps` | +| "self-correcting / retry on failure" | `CorrectionOps` | +| "run in a side/fork repo" | `SideRepoOps` | +| "write a spec before implementing" | `SpecOps` | +| "A/B test workflow variants" | `TrialOps` | +| "process items from a queue" | `WorkQueueOps` | +| "deterministic, no LLM needed" | `DeterministicOps` | +| "manage from a central repo" | `CentralRepoOps` | +| "track work via GitHub Projects" | `Monitoring with Projects` | + +### Integration Auth Mapping + +When the user names a third-party service or MCP server: + +1. Confirm whether native tool, MCP server, or safe-output job is the right integration path. +2. Look up the integration's auth requirements and required scopes before finalizing the design. +3. Provide a concrete setup checklist with: + - required GitHub Actions secrets (names to create) + - workflow env variables that consume those secrets + - minimum token scopes/permissions needed + +Output format to use: + +```text +Integration auth setup: +- : + - Secrets to create: , + - Workflow env vars: =${{ secrets. }} + - Required scopes/permissions: +``` + +Never suggest committing plaintext tokens. + +### Data Strategy Mapping + +| User says... | Maps to | +|---|---| +| "analyze PRs", "review issues", "check status" | add `steps:` that pre-fetch with `gh` + `jq` | +| "read the diff", "look at changed files" | add `steps:` using `gh pr diff` or `gh pr view --json files` | +| "search for patterns across repos" | add `steps:` using `gh search` + `jq` filters | +| "just respond to a comment" | no pre-fetch needed (event payload is enough) | +| "process each item individually" | suggest sub-agent pattern with `model: small` | + +## Token Optimization Defaults + +Apply these defaults unless the user explicitly asks otherwise: + +1. Use DataOps by default for GitHub reads: pre-fetch/aggregate with `gh` + `jq` in `steps:`, store compact JSON in `/tmp/gh-aw/data/`, and point the prompt to those files (see `.github/aw/token-optimization.md` for details). +2. Keep tool surface minimal: default to `tools.github.mode: gh-proxy`, include only required toolsets, and prefer `bash` + `gh` for simple reads. +3. For batch workloads, split items into compact data and suggest sub-agent processing with `model: small`. +4. Keep prompts compact: concise imperative instructions, explicit file paths, single-line `noop` guidance, and stable instructions before dynamic content. + +## Progressive Disclosure Rules + +1. Never dump all options at once; ask one targeted question at a time. +2. Skip questions when answers are inferable from prior user statements. +3. Offer smart defaults and request confirmation instead of over-questioning. +4. Ask at most 5 questions before presenting a summary; then ask "anything else?" if needed. +5. Detect done signals (`that's it`, `looks good`, `generate it`) and proceed to generation. + +## Confirmation Format + +Use this exact structure: + +```text +📋 Proposed workflow: +- Name: +- Trigger: +- Engine: +- Tools: +- Safe outputs: +- Network: +- Integrations/Auth: +- Deployment: +- Intent: +``` + +Then ask: **"Ready to generate, or want to adjust anything?"** + +## Generation Template + +After confirmation, generate one workflow file using the same skeleton style as `.github/aw/create-agentic-workflow.md`. + +```markdown +--- +emoji: +description: +on: + +permissions: + contents: read + issues: read + pull-requests: read +tools: + github: + mode: gh-proxy + toolsets: [default] +steps: + - name: + run: | + mkdir -p /tmp/gh-aw/data + +safe-outputs: + +network: + allowed: + - defaults + - +--- + +# + +## Task + + +If `steps:` includes pre-fetch commands, read the resulting `/tmp/gh-aw/data/*.json` files instead of broad live re-fetches. + +## Safe Outputs + +- Use configured safe outputs for all visible write actions. +- Call `noop` with a short reason when no action is needed. +``` + +## Validation Checklist + +Before final output, run this internal self-check: + +- [ ] Agent job permissions remain read-only (writes only via safe outputs) +- [ ] `safe-outputs:` covers every write action mentioned in prompt/instructions +- [ ] Network access is scoped; avoid blanket wildcard entries +- [ ] Trigger matches the user's intended activation event +- [ ] Prompt instructs agent to call `noop` when no action is needed +- [ ] Unnecessary defaults are omitted (for example `engine: copilot`) +- [ ] If reading GitHub data, `steps:` pre-fetches compact JSON (DataOps) +- [ ] `tools.github.mode` is `gh-proxy` unless broader MCP toolsets are explicitly needed +- [ ] Only required toolsets are listed (avoid blanket toolset lists) +- [ ] Prompt references specific pre-computed file paths +- [ ] For batch processing (>5 items), sub-agent pattern is suggested +- [ ] For each third-party service/MCP integration, required secrets/env vars are listed +- [ ] Auth guidance includes least-privilege token scope recommendations +- [ ] For GHEC/GHES deployments, `engine.api-target` and GHES compatibility guidance are included when needed + +## References (load only when needed) + +In-repo references: +- `.github/aw/syntax.md` (index → `.github/aw/syntax-core.md`, `.github/aw/syntax-agentic.md`, `.github/aw/syntax-tools-imports.md`) +- `.github/aw/safe-outputs.md` (index → `.github/aw/safe-outputs-content.md`, `.github/aw/safe-outputs-management.md`, `.github/aw/safe-outputs-automation.md`, `.github/aw/safe-outputs-runtime.md`) +- `.github/aw/network.md` +- `.github/aw/patterns.md` +- `.github/aw/subagents.md` +- `.github/aw/token-optimization.md` +- `.github/aw/triggers.md` +- `.github/aw/create-agentic-workflow.md` + +Portable HTTPS references: +- `https://github.com/github/gh-aw/blob/main/.github/aw/syntax.md` (index → `.../syntax-core.md`, `.../syntax-agentic.md`, `.../syntax-tools-imports.md`) +- `https://github.com/github/gh-aw/blob/main/.github/aw/safe-outputs.md` (index → `.../safe-outputs-content.md`, `.../safe-outputs-management.md`, `.../safe-outputs-automation.md`, `.../safe-outputs-runtime.md`) +- `https://github.com/github/gh-aw/blob/main/.github/aw/network.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/patterns.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/triggers.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/create-agentic-workflow.md` diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index b4505045ca..ee714d339d 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -32,6 +32,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/memory.md` - `.github/aw/messages.md` - `.github/aw/network.md` +- `.github/aw/optimize-agentic-workflow.md` - `.github/aw/patterns.md` - `.github/aw/pr-reviewer.md` - `.github/aw/report.md` diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml index afd36f7d17..bf346b00a9 100644 --- a/.github/workflows/Windows.yml +++ b/.github/workflows/Windows.yml @@ -28,7 +28,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v3 - run: | @@ -37,9 +37,10 @@ jobs: ${{ matrix.cmd1 }} ${{ matrix.cmd2 }} ${{ matrix.cmd3 }} - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.arch }} - cmake ${{ matrix.bindings }} -G "NMake Makefiles" ../ - nmake + for /f "usebackq delims=" %%i in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i" + call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.arch }} || exit /b 1 + cmake ${{ matrix.bindings }} -G "NMake Makefiles" ../ || exit /b 1 + nmake || exit /b 1 cd .. shell: cmd - name: Run Regressions @@ -52,7 +53,8 @@ jobs: if: ${{ matrix.test }} run: | pushd build - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.arch }} + for /f "usebackq delims=" %%i in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i" + call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.arch }} || exit /b 1 pushd build\python python z3test.py z3 python z3test.py z3num diff --git a/.github/workflows/a3-python.lock.yml b/.github/workflows/a3-python.lock.yml index 0e74c7eb6d..47e6446ec0 100644 --- a/.github/workflows/a3-python.lock.yml +++ b/.github/workflows/a3-python.lock.yml @@ -31,13 +31,13 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -91,7 +91,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -152,7 +152,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -377,7 +377,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -398,7 +398,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -999,7 +999,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1194,7 +1194,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1222,7 +1222,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1454,7 +1454,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/academic-citation-tracker.lock.yml b/.github/workflows/academic-citation-tracker.lock.yml index c59fc12bd0..334003e1a8 100644 --- a/.github/workflows/academic-citation-tracker.lock.yml +++ b/.github/workflows/academic-citation-tracker.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"480bf21bcee3122e341b8d9cc8b19279eaa3c25109c5268eb353b9cf2a749663","body_hash":"05745b276b67f33e54e95f20396a0d79e1bf2384cd2d43bc3b31b6ca3ddae969","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} -# 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":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"v9","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.79.6","version":"v0.79.6"}],"resolution_failures":[{"repo":"actions/github-script","ref":"v9","error_type":"dynamic_resolution_failed"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"480bf21bcee3122e341b8d9cc8b19279eaa3c25109c5268eb353b9cf2a749663","body_hash":"05745b276b67f33e54e95f20396a0d79e1bf2384cd2d43bc3b31b6ca3ddae969","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# 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":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"v9","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.79.8","version":"v0.79.8"}],"resolution_failures":[{"repo":"actions/github-script","ref":"v9","error_type":"dynamic_resolution_failed"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.79.6). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,15 +32,15 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.79.8 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -78,9 +79,9 @@ jobs: outputs: comment_id: "" comment_repo: "" - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} 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 }} @@ -92,7 +93,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -111,7 +112,7 @@ jobs: GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AGENT_VERSION: "1.0.60" - GH_AW_INFO_CLI_VERSION: "v0.79.6" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "Academic Citation & Research Trend Tracker" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" @@ -153,7 +154,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -189,7 +190,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.79.6" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -337,7 +338,6 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt @@ -351,7 +351,7 @@ jobs: agent: needs: activation - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: read-all concurrency: @@ -385,7 +385,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -406,7 +406,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -419,7 +419,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -702,7 +702,7 @@ jobs: 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.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_c6fee03c27b97257_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -774,9 +774,11 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -784,7 +786,6 @@ jobs: (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/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.semanticscholar.org\",\"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\",\"export.arxiv.org\",\"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\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"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*\",\"google/nano-banana*\",\"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-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\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"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\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"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.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" @@ -810,12 +811,11 @@ jobs: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.79.6 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -830,7 +830,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Detect agent errors if: always() id: detect-agent-errors @@ -1018,7 +1017,7 @@ jobs: - update_cache_memory if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: read @@ -1036,7 +1035,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1196,9 +1195,9 @@ jobs: 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "false" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1230,7 +1229,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1258,7 +1257,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1286,7 +1285,7 @@ jobs: 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 "$HOME/.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' @@ -1344,9 +1343,10 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -1354,7 +1354,6 @@ jobs: (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/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\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" @@ -1383,7 +1382,7 @@ jobs: GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.79.6 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1397,7 +1396,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1488,7 +1486,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1564,7 +1562,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index fbd398547b..2e3a02c579 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -1,3 +1,5 @@ +# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.80.9). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -12,7 +14,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.79.6). DO NOT EDIT. # # To regenerate this workflow, run: # gh aw compile @@ -20,15 +21,17 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Alternative regeneration methods: -# make recompile +# This file defines the generated agentic maintenance workflow for this repository. +# It runs scheduled cleanup for expiring safe outputs and supports manual maintenance operations. # -# Or use the gh-aw CLI directly: -# ./gh-aw compile --validate --verbose +# This workflow is generated automatically when workflows use expiring safe outputs +# or when repository maintenance features are enabled in .github/workflows/aw.json. # -# The workflow is generated when any workflow uses the 'expires' field -# in create-discussions, create-issues, or create-pull-request safe-outputs configuration. -# Schedule frequency is automatically determined by the shortest expiration time. +# To disable maintenance workflow generation, set in .github/workflows/aw.json: +# {"maintenance": false} +# +# Agentic maintenance docs: +# https://github.github.com/gh-aw/reference/ephemerals/#manual-maintenance-operations # name: Agentic Maintenance @@ -93,7 +96,7 @@ jobs: pull-requests: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -131,7 +134,7 @@ jobs: actions: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -155,12 +158,12 @@ jobs: operation: ${{ steps.record.outputs.operation }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -175,9 +178,9 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup-cli@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: - version: v0.79.6 + version: v0.80.9 - name: Run operation uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -195,7 +198,9 @@ jobs: - name: Record outputs id: record - run: echo "operation=${{ inputs.operation }}" >> "$GITHUB_OUTPUT" + env: + GH_AW_OPERATION: ${{ inputs.operation }} + run: echo "operation=$GH_AW_OPERATION" >> "$GITHUB_OUTPUT" update_pull_request_branches: if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'update_pull_request_branches' && (!(github.event.repository.fork)) }} @@ -205,7 +210,7 @@ jobs: pull-requests: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -244,14 +249,14 @@ jobs: run_url: ${{ steps.record.outputs.run_url }} steps: - name: Checkout actions folder - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | actions persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -280,7 +285,9 @@ jobs: - name: Record outputs id: record - run: echo "run_url=${{ inputs.run_url }}" >> "$GITHUB_OUTPUT" + env: + GH_AW_RUN_URL: ${{ inputs.run_url }} + run: echo "run_url=$GH_AW_RUN_URL" >> "$GITHUB_OUTPUT" create_labels: if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels' && (!(github.event.repository.fork)) }} @@ -290,12 +297,12 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -310,9 +317,9 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup-cli@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: - version: v0.79.6 + version: v0.80.9 - name: Create missing labels uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -336,12 +343,12 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -356,13 +363,13 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup-cli@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: - version: v0.79.6 + version: v0.80.9 - name: Restore activity report logs cache id: activity_report_logs_cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ./.cache/gh-aw/activity-report-logs key: ${{ runner.os }}-activity-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} @@ -379,10 +386,10 @@ jobs: ${GH_AW_CMD_PREFIX} logs \ --repo "${{ github.repository }}" \ --start-date -1w \ - --count 100 \ + --count 500 \ --output ./.cache/gh-aw/activity-report-logs \ --format markdown \ - > ./.cache/gh-aw/activity-report-logs/report.md + --report-file ./.cache/gh-aw/activity-report-logs/report.md - name: Save activity report logs cache if: ${{ always() }} @@ -441,12 +448,12 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -461,13 +468,13 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup-cli@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: - version: v0.79.6 + version: v0.80.9 - name: Restore forecast report logs cache id: forecast_report_logs_cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ./.github/aw/logs key: ${{ runner.os }}-forecast-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} @@ -538,7 +545,7 @@ jobs: issues: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -570,12 +577,12 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -590,9 +597,9 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 + uses: github/gh-aw-actions/setup-cli@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9 with: - version: v0.79.6 + version: v0.80.9 - name: Validate workflows and file issue on findings uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index d88450f827..6eac0e3ab8 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Configure CMake and build run: | diff --git a/.github/workflows/api-coherence-checker.lock.yml b/.github/workflows/api-coherence-checker.lock.yml index 7dd3083bf5..5f402f213c 100644 --- a/.github/workflows/api-coherence-checker.lock.yml +++ b/.github/workflows/api-coherence-checker.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"834e887ef6def1de97d035da77ac91ab4abdaa02e16edd0e7437f6df4fd4fdc7","body_hash":"a3ec39bff49a3afd8f6e9c2bfdb45095d580f2933ae084824133687c651fd10a","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} -# 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/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"v9","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.79.6","version":"v0.79.6"}],"resolution_failures":[{"repo":"actions/github-script","ref":"v9","error_type":"dynamic_resolution_failed"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"834e887ef6def1de97d035da77ac91ab4abdaa02e16edd0e7437f6df4fd4fdc7","body_hash":"a3ec39bff49a3afd8f6e9c2bfdb45095d580f2933ae084824133687c651fd10a","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# 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/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"v9","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.79.8","version":"v0.79.8"}],"resolution_failures":[{"repo":"actions/github-script","ref":"v9","error_type":"dynamic_resolution_failed"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.79.6). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,16 +32,16 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.79.8 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -80,9 +81,9 @@ jobs: outputs: comment_id: "" comment_repo: "" - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} 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 }} @@ -94,7 +95,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -113,7 +114,7 @@ jobs: GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AGENT_VERSION: "1.0.60" - GH_AW_INFO_CLI_VERSION: "v0.79.6" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "API Coherence Checker" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" @@ -155,7 +156,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -191,7 +192,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.79.6" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -339,7 +340,6 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt @@ -353,7 +353,7 @@ jobs: agent: needs: activation - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: read-all concurrency: @@ -387,7 +387,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -414,7 +414,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -422,7 +422,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -702,7 +702,7 @@ jobs: 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.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_c6fee03c27b97257_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -774,9 +774,11 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -784,7 +786,6 @@ jobs: (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/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,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"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*\",\"google/nano-banana*\",\"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-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\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"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\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"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.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" @@ -810,12 +811,11 @@ jobs: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 30 - GH_AW_VERSION: v0.79.6 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -830,7 +830,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Detect agent errors if: always() id: detect-agent-errors @@ -1017,7 +1016,7 @@ jobs: - update_cache_memory if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: read @@ -1035,7 +1034,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1194,9 +1193,9 @@ jobs: 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "false" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1228,7 +1227,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1256,7 +1255,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1284,7 +1283,7 @@ jobs: 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 "$HOME/.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' @@ -1342,9 +1341,10 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -1352,7 +1352,6 @@ jobs: (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/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\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" @@ -1381,7 +1380,7 @@ jobs: GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.79.6 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1395,7 +1394,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1486,7 +1484,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1562,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/build-warning-fixer.lock.yml b/.github/workflows/build-warning-fixer.lock.yml index e4bca92c59..c8bfa9ae9b 100644 --- a/.github/workflows/build-warning-fixer.lock.yml +++ b/.github/workflows/build-warning-fixer.lock.yml @@ -32,13 +32,13 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -92,7 +92,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -153,7 +153,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -376,7 +376,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -397,7 +397,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1003,7 +1003,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1196,7 +1196,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1224,7 +1224,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1456,7 +1456,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1500,7 +1500,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1508,7 +1508,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ steps.extract-base-branch.outputs.base-branch || 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 }} diff --git a/.github/workflows/build-z3-cache.yml b/.github/workflows/build-z3-cache.yml index 496dc998e1..3898184c7e 100644 --- a/.github/workflows/build-z3-cache.yml +++ b/.github/workflows/build-z3-cache.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -45,7 +45,7 @@ jobs: - name: Restore or create cache id: cache-z3 - uses: actions/cache@v5.0.5 + uses: actions/cache@v6.0.0 with: path: | build/z3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8370d966a0..67ed3e4796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: runRegressions: false steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -81,7 +81,7 @@ jobs: container: "quay.io/pypa/manylinux_2_34_x86_64:latest" steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Select Python run: | @@ -121,7 +121,7 @@ jobs: container: quay.io/pypa/manylinux_2_28_x86_64:latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download ARM toolchain run: curl -L -o /tmp/arm-toolchain.tar.xz 'https://developer.arm.com/-/media/Files/downloads/gnu/13.3.rel1/binrel/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu.tar.xz' @@ -165,7 +165,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup OCaml uses: ocaml/setup-ocaml@v3 @@ -220,7 +220,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup OCaml uses: ocaml/setup-ocaml@v3 @@ -248,7 +248,19 @@ jobs: cd .. - name: Install Z3 OCaml package - run: eval `opam config env`; ocamlfind install z3-static build/api/ml/* build/libz3-static.a + run: | + eval `opam config env` + ocamlfind install z3-static build/api/ml/* build/libz3-static.a + # Ensure the stublibs directory where dllz3ml-static.so was installed is + # listed in ld.conf so the OCaml bytecode linker can find it. When no + # explicit -dll flag is passed, ocamlfind installs dll* files to stublibs + # but may not update ld.conf, causing "Fatal error: exception End_of_file" + # in ocamlc when it cannot locate the stub shared library at link time. + STUBLIBS="$(ocamlfind printconf destdir)/stublibs" + LDCONF="$(ocamlfind printconf ldconf)" + if [ -d "$STUBLIBS" ] && ! grep -qF "$STUBLIBS" "$LDCONF" 2>/dev/null; then + echo "$STUBLIBS" >> "$LDCONF" + fi - name: Build and run OCaml examples run: | @@ -314,7 +326,7 @@ jobs: runTests: false steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -404,7 +416,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -453,7 +465,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -494,7 +506,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -514,7 +526,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 diff --git a/.github/workflows/code-conventions-analyzer.lock.yml b/.github/workflows/code-conventions-analyzer.lock.yml index 1886902fe5..09a87d5f21 100644 --- a/.github/workflows/code-conventions-analyzer.lock.yml +++ b/.github/workflows/code-conventions-analyzer.lock.yml @@ -31,15 +31,15 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -93,7 +93,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -154,7 +154,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -381,7 +381,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -402,7 +402,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -415,7 +415,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1087,7 +1087,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1281,7 +1281,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1309,7 +1309,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1541,7 +1541,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1617,7 +1617,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index dbe8dd7ff8..e597b670f2 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -34,13 +34,13 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -97,7 +97,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -163,7 +163,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -393,7 +393,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -415,7 +415,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1021,7 +1021,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1224,7 +1224,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1253,7 +1253,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1457,7 +1457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1535,7 +1535,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1580,7 +1580,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1588,7 +1588,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ steps.extract-base-branch.outputs.base-branch || 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 }} diff --git a/.github/workflows/compare-stats-anomaly-reporter.lock.yml b/.github/workflows/compare-stats-anomaly-reporter.lock.yml index f965c8b187..feb33778ef 100644 --- a/.github/workflows/compare-stats-anomaly-reporter.lock.yml +++ b/.github/workflows/compare-stats-anomaly-reporter.lock.yml @@ -31,13 +31,13 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -90,7 +90,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -151,7 +151,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -377,7 +377,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -398,7 +398,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -990,7 +990,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1183,7 +1183,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1211,7 +1211,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1441,7 +1441,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 25327e88fa..a2742f2d9f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,7 +19,7 @@ jobs: COV_DETAILS_PATH: ${{github.workspace}}/cov-details steps: - - uses: actions/checkout@v6.0.3 + - uses: actions/checkout@v7.0.0 - name: Setup run: | diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index db3a10855b..49eb7f1819 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Install cross build tools run: apt update && apt install -y ninja-build cmake python3 g++-13-${{ matrix.arch }}-linux-gnu diff --git a/.github/workflows/csa-analysis.lock.yml b/.github/workflows/csa-analysis.lock.yml index d000d23f5d..847be785ba 100644 --- a/.github/workflows/csa-analysis.lock.yml +++ b/.github/workflows/csa-analysis.lock.yml @@ -31,16 +31,16 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -94,7 +94,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -155,7 +155,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -387,7 +387,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -414,7 +414,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -422,7 +422,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1035,7 +1035,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1229,7 +1229,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1257,7 +1257,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1487,7 +1487,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1563,7 +1563,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bcbc2398ac..153a6ee5dc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Go uses: actions/setup-go@v6 @@ -46,7 +46,7 @@ jobs: needs: build-go-docs steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/fstar-master-build.yml b/.github/workflows/fstar-master-build.yml index c60d060a10..fe229a153d 100644 --- a/.github/workflows/fstar-master-build.yml +++ b/.github/workflows/fstar-master-build.yml @@ -56,7 +56,7 @@ jobs: DISCUSSION_CATEGORY: ${{ github.event.inputs.discussion_category || 'Agentic Workflows' }} steps: - name: Checkout Z3 - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 with: ref: ${{ env.Z3_REF }} fetch-depth: 1 diff --git a/.github/workflows/issue-backlog-processor.lock.yml b/.github/workflows/issue-backlog-processor.lock.yml index 743436abc7..d5dace3bf9 100644 --- a/.github/workflows/issue-backlog-processor.lock.yml +++ b/.github/workflows/issue-backlog-processor.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7671ab751a3f717291cd8f191c4619897acdb7e712fc38c87b9806d95f2b1e0f","body_hash":"0c085cd0722df29959ce10ad54f82dea6ecc84782a1f749d14ad8c1d000b7a6f","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} -# 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":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"v9","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.79.6","version":"v0.79.6"}],"resolution_failures":[{"repo":"actions/github-script","ref":"v9","error_type":"dynamic_resolution_failed"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7671ab751a3f717291cd8f191c4619897acdb7e712fc38c87b9806d95f2b1e0f","body_hash":"0c085cd0722df29959ce10ad54f82dea6ecc84782a1f749d14ad8c1d000b7a6f","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# 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":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"v9","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.79.8","version":"v0.79.8"}],"resolution_failures":[{"repo":"actions/github-script","ref":"v9","error_type":"dynamic_resolution_failed"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.79.6). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -31,15 +32,15 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.79.8 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -79,9 +80,9 @@ jobs: outputs: comment_id: "" comment_repo: "" - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} 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 }} @@ -93,7 +94,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -112,7 +113,7 @@ jobs: GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AGENT_VERSION: "1.0.60" - GH_AW_INFO_CLI_VERSION: "v0.79.6" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "Issue Backlog Processor" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" @@ -154,7 +155,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -190,7 +191,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.79.6" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -338,7 +339,6 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt @@ -352,7 +352,7 @@ jobs: agent: needs: activation - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest permissions: read-all concurrency: @@ -386,7 +386,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -407,7 +407,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -420,7 +420,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -723,7 +723,7 @@ jobs: 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.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_c6fee03c27b97257_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -795,9 +795,11 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -805,7 +807,6 @@ jobs: (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/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,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"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*\",\"google/nano-banana*\",\"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-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\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"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\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"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.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" @@ -831,12 +832,11 @@ jobs: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.79.6 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -851,7 +851,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Detect agent errors if: always() id: detect-agent-errors @@ -1038,7 +1037,7 @@ jobs: - update_cache_memory if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: read @@ -1057,7 +1056,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1216,9 +1215,9 @@ jobs: 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "false" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1250,7 +1249,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1278,7 +1277,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1306,7 +1305,7 @@ jobs: 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 "$HOME/.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' @@ -1364,9 +1363,10 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -1374,7 +1374,6 @@ jobs: (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/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\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" @@ -1403,7 +1402,7 @@ jobs: GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.79.6 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1417,7 +1416,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1511,7 +1509,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1587,7 +1585,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/memory-safety-report.lock.yml b/.github/workflows/memory-safety-report.lock.yml index fb9cb5b363..06242414b3 100644 --- a/.github/workflows/memory-safety-report.lock.yml +++ b/.github/workflows/memory-safety-report.lock.yml @@ -34,16 +34,16 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -110,7 +110,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -173,7 +173,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -415,7 +415,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -442,7 +442,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -450,7 +450,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1076,7 +1076,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1268,7 +1268,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1296,7 +1296,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1500,7 +1500,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1561,7 +1561,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1637,7 +1637,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/memory-safety.yml b/.github/workflows/memory-safety.yml index 7e68dcd98d..6c62a93d2d 100644 --- a/.github/workflows/memory-safety.yml +++ b/.github/workflows/memory-safety.yml @@ -31,7 +31,7 @@ jobs: ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0:print_stats=1:log_path=/tmp/asan" steps: - name: Checkout repository - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -121,7 +121,7 @@ jobs: UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=0:log_path=/tmp/ubsan" steps: - name: Checkout repository - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 diff --git a/.github/workflows/msvc-static-build-clang-cl.yml b/.github/workflows/msvc-static-build-clang-cl.yml index e6545c1d03..c2ba9b901a 100644 --- a/.github/workflows/msvc-static-build-clang-cl.yml +++ b/.github/workflows/msvc-static-build-clang-cl.yml @@ -14,7 +14,7 @@ jobs: BUILD_TYPE: Release steps: - name: Checkout Repo - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Build run: | diff --git a/.github/workflows/msvc-static-build.yml b/.github/workflows/msvc-static-build.yml index 6a3256bf73..3bca31eb2b 100644 --- a/.github/workflows/msvc-static-build.yml +++ b/.github/workflows/msvc-static-build.yml @@ -14,7 +14,7 @@ jobs: BUILD_TYPE: Release steps: - name: Checkout Repo - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Build run: | diff --git a/.github/workflows/nightly-validation.yml b/.github/workflows/nightly-validation.yml index f4daced791..f252f1a4a0 100644 --- a/.github/workflows/nightly-validation.yml +++ b/.github/workflows/nightly-validation.yml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -87,7 +87,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -142,7 +142,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -197,7 +197,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -256,7 +256,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download Windows x64 build from release env: @@ -292,7 +292,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download Windows x86 build from release env: @@ -328,7 +328,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download Ubuntu x64 build from release env: @@ -361,7 +361,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download macOS x64 build from release env: @@ -394,7 +394,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download macOS ARM64 build from release env: @@ -431,7 +431,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -470,7 +470,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -510,7 +510,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -553,7 +553,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -582,7 +582,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -611,7 +611,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -640,7 +640,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -672,7 +672,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -727,7 +727,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download macOS x64 build from release env: @@ -779,7 +779,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download macOS ARM64 build from release env: @@ -835,7 +835,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -844,3 +844,97 @@ jobs: - name: Run build script unit tests run: python -m unittest discover -s scripts/tests -p "test_*.py" -v + + # ============================================================================ + # DOTNET MANAGED WRAPPER ARCHITECTURE VALIDATION + # ============================================================================ + + validate-dotnet-anycpu: + name: "Validate Microsoft.Z3.dll is AnyCPU (issue #9863)" + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + timeout-minutes: 15 + steps: + - name: Checkout code + uses: actions/checkout@v7.0.0 + + - name: Download NuGet package 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 "*.nupkg" --dir nuget-packages + + - name: Extract managed DLL from NuGet package + run: | + # NuGet packages are ZIP archives; exclude the symbols package + NUPKG=$(ls nuget-packages/*.nupkg | grep -v '\.symbols\.' | grep -v '\.snupkg' | head -n 1) + echo "Checking package: $NUPKG" + unzip -q "$NUPKG" "lib/netstandard2.0/Microsoft.Z3.dll" -d nupkg-extracted + + - name: Check PE Machine field is AnyCPU (not architecture-specific) + run: | + python3 - <<'EOF' + import struct + import sys + + dll_path = "nupkg-extracted/lib/netstandard2.0/Microsoft.Z3.dll" + + with open(dll_path, 'rb') as f: + # Verify MZ magic + if f.read(2) != b'MZ': + print("ERROR: Not a valid PE file (missing MZ header)") + sys.exit(1) + + # Read PE header offset stored at 0x3C in the DOS stub + f.seek(0x3C) + pe_offset = struct.unpack('> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1441,7 +1441,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/pyodide-pypi.yml b/.github/workflows/pyodide-pypi.yml new file mode 100644 index 0000000000..8ae3f6f16d --- /dev/null +++ b/.github/workflows/pyodide-pypi.yml @@ -0,0 +1,57 @@ +name: Pyodide Wheel (PyPI) + +# Builds a PEP 783 `pyemscripten_*_wasm32` wheel for z3-solver using cibuildwheel +# and publishes it to PyPI on tag pushes. Unlike the legacy pyodide.yml (which +# uses `pyodide build` and produces a Pyodide-version-locked emscripten_* wheel +# uploaded only as a CI artifact), this wheel is installable at runtime with +# micropip from PyPI. See src/api/python/pyproject.toml [tool.cibuildwheel]. + +on: + release: + types: [created] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-pyodide: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7.0.0 + + - name: Build Pyodide wheel + uses: pypa/cibuildwheel@v4.1.0 + with: + # The Python bindings live in a subdirectory of the repo. + package-dir: src/api/python + env: + CIBW_PLATFORM: pyodide + # Exception/longjmp/bigint flags are declared in + # src/api/python/pyproject.toml ([tool.pyodide.build]) and combined + # with Pyodide's -fwasm-exceptions defaults. Don't set CFLAGS/CXXFLAGS + # here — a JS-EH -fexceptions value conflicts with the wasm-EH ABI. + + - name: Store Pyodide wheel + uses: actions/upload-artifact@v7 + with: + name: pyodide-wheel + path: wheelhouse/*.whl + + publish: + name: Publish to PyPI + runs-on: ubuntu-24.04 + if: startsWith(github.ref, 'refs/tags/') + needs: [build-pyodide] + environment: release + permissions: + id-token: write # trusted publishing (OIDC), no API token needed + steps: + - name: Download Pyodide wheel + uses: actions/download-artifact@v8 + with: + name: pyodide-wheel + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/pyodide.yml b/.github/workflows/pyodide.yml deleted file mode 100644 index 646a95fd70..0000000000 --- a/.github/workflows/pyodide.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Pyodide Build - -on: - schedule: - - cron: '0 0 */2 * *' - workflow_dispatch: - -env: - BUILD_TYPE: Release - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-24.04 - - strategy: - fail-fast: false - - steps: - - name: Checkout code - uses: actions/checkout@v6.0.3 - - - 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 CMake and build - 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 Z3 - run: | - source ~/emsdk/emsdk_env.sh - cd src/api/python - CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" CXXFLAG="${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: Setup z3 wheel - run: | - ~/env-pyodide/bin/pip install src/api/python/dist/*.whl - ~/env-pyodide/bin/python - > "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1445,7 +1445,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/release-notes-updater.lock.yml b/.github/workflows/release-notes-updater.lock.yml index d6e74d278e..4bc382a192 100644 --- a/.github/workflows/release-notes-updater.lock.yml +++ b/.github/workflows/release-notes-updater.lock.yml @@ -31,14 +31,14 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -92,7 +92,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -153,7 +153,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -378,7 +378,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -405,7 +405,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false @@ -993,7 +993,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1185,7 +1185,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1213,7 +1213,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1443,7 +1443,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 112b47960f..07387885bc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -78,7 +78,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -122,7 +122,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download macOS x64 Build uses: actions/download-artifact@v8.0.1 @@ -181,7 +181,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download macOS ARM64 Build uses: actions/download-artifact@v8.0.1 @@ -239,7 +239,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -268,7 +268,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -303,7 +303,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -359,7 +359,7 @@ jobs: container: quay.io/pypa/manylinux_2_28_x86_64:latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Select Python run: | @@ -397,7 +397,7 @@ jobs: container: quay.io/pypa/manylinux_2_28_x86_64:latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download ARM toolchain run: curl -L -o /tmp/arm-toolchain.tar.xz 'https://developer.arm.com/-/media/Files/downloads/gnu/13.3.rel1/binrel/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu.tar.xz' @@ -445,7 +445,7 @@ jobs: container: quay.io/pypa/manylinux_2_28_x86_64:latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - 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' @@ -493,66 +493,13 @@ jobs: 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.3 - - - 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 - with: - name: PyodidePythonBuild - path: src/api/python/dist/*.whl - retention-days: 7 - windows-build-x64: name: "Windows x64 build" runs-on: windows-latest timeout-minutes: 120 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -562,7 +509,8 @@ jobs: - name: Build shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + for /f "usebackq delims=" %%i in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i" + call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" x64 || exit /b 1 python scripts\mk_win_dist.py --x64-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --zip - name: Upload artifact @@ -578,7 +526,7 @@ jobs: timeout-minutes: 120 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -588,7 +536,8 @@ jobs: - name: Build shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86 + for /f "usebackq delims=" %%i in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i" + call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" x86 || exit /b 1 python scripts\mk_win_dist.py --x86-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --zip - name: Upload artifact @@ -604,7 +553,7 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -614,7 +563,8 @@ jobs: - name: Build shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" amd64_arm64 + for /f "usebackq delims=" %%i in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i" + call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" amd64_arm64 || exit /b 1 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 @@ -634,7 +584,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -709,7 +659,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -750,11 +700,11 @@ 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, manylinux-python-riscv64, pyodide-python] + 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] runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup Python uses: actions/setup-python@v6 @@ -809,12 +759,6 @@ jobs: 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 @@ -873,7 +817,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download all artifacts uses: actions/download-artifact@v8.0.1 @@ -929,7 +873,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Download NuGet packages uses: actions/download-artifact@v8.0.1 diff --git a/.github/workflows/smtlib-benchmark-finder.lock.yml b/.github/workflows/smtlib-benchmark-finder.lock.yml index 193bdc6ad9..6ce0fd5357 100644 --- a/.github/workflows/smtlib-benchmark-finder.lock.yml +++ b/.github/workflows/smtlib-benchmark-finder.lock.yml @@ -31,15 +31,15 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -92,7 +92,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -153,7 +153,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -385,7 +385,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -406,7 +406,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -419,7 +419,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1036,7 +1036,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1230,7 +1230,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1258,7 +1258,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1488,7 +1488,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1564,7 +1564,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/specbot-crash-analyzer.lock.yml b/.github/workflows/specbot-crash-analyzer.lock.yml index ff34c40e5f..db5ec29ba1 100644 --- a/.github/workflows/specbot-crash-analyzer.lock.yml +++ b/.github/workflows/specbot-crash-analyzer.lock.yml @@ -31,16 +31,16 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -91,7 +91,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -152,7 +152,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -383,7 +383,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -410,7 +410,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout c3 branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false ref: c3 @@ -460,7 +460,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1073,7 +1073,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1267,7 +1267,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1295,7 +1295,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1525,7 +1525,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1601,7 +1601,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/tactic-to-simplifier.lock.yml b/.github/workflows/tactic-to-simplifier.lock.yml index 2e3a8767c2..9a6eb38dc2 100644 --- a/.github/workflows/tactic-to-simplifier.lock.yml +++ b/.github/workflows/tactic-to-simplifier.lock.yml @@ -31,16 +31,16 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -94,7 +94,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -155,7 +155,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -386,7 +386,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -413,7 +413,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -421,7 +421,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1043,7 +1043,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1234,7 +1234,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1262,7 +1262,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1493,7 +1493,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1569,7 +1569,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/tptp-benchmark.lock.yml b/.github/workflows/tptp-benchmark.lock.yml index 9f8035d569..2d46509997 100644 --- a/.github/workflows/tptp-benchmark.lock.yml +++ b/.github/workflows/tptp-benchmark.lock.yml @@ -31,14 +31,14 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -91,7 +91,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -152,7 +152,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -379,7 +379,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -406,7 +406,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Install build dependencies @@ -999,7 +999,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1192,7 +1192,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1220,7 +1220,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1450,7 +1450,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/wasm-release.yml b/.github/workflows/wasm-release.yml index cc71a37157..a34346a4f6 100644 --- a/.github/workflows/wasm-release.yml +++ b/.github/workflows/wasm-release.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index b6e41d7e24..740b3b7b1a 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/wip.yml b/.github/workflows/wip.yml index b001a959c7..6ed1a79287 100644 --- a/.github/workflows/wip.yml +++ b/.github/workflows/wip.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.3 + - uses: actions/checkout@v7.0.0 - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} diff --git a/.github/workflows/workflow-suggestion-agent.lock.yml b/.github/workflows/workflow-suggestion-agent.lock.yml index 5e4de8725f..a7d48b014d 100644 --- a/.github/workflows/workflow-suggestion-agent.lock.yml +++ b/.github/workflows/workflow-suggestion-agent.lock.yml @@ -31,16 +31,16 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -94,7 +94,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -155,7 +155,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -387,7 +387,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -414,7 +414,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -422,7 +422,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1035,7 +1035,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1228,7 +1228,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1256,7 +1256,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1486,7 +1486,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1562,7 +1562,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/zipt-code-reviewer.lock.yml b/.github/workflows/zipt-code-reviewer.lock.yml index 812fd2759c..81a7e7e5ed 100644 --- a/.github/workflows/zipt-code-reviewer.lock.yml +++ b/.github/workflows/zipt-code-reviewer.lock.yml @@ -31,16 +31,16 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@v9 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.79.6 +# - github/gh-aw-actions/setup@v0.80.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -93,7 +93,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -154,7 +154,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -383,7 +383,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -410,7 +410,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -418,7 +418,7 @@ jobs: - 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 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory @@ -1063,7 +1063,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1255,7 +1255,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1283,7 +1283,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1514,7 +1514,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1590,7 +1590,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.79.6 + uses: github/gh-aw-actions/setup@v0.80.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.gitignore b/.gitignore index 9cad945c26..8a480f02cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ *~ rebase.cmd +reports/ +crashes/ *.pyc *.pyo # Ignore callgrind files callgrind.out.* +gmon.out # .hpp files are automatically generated *.hpp .env diff --git a/BUILD.bazel b/BUILD.bazel index f4d69a747e..ef78b6e83e 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -19,14 +19,23 @@ filegroup( cmake( name = "z3_dynamic", generate_args = [ - "-G Ninja", + "-G Ninja", "-D Z3_EXPORTED_TARGETS=", # prevents installation, leaving symlinks between dylibs intact on copy ], lib_source = ":all_files", out_binaries = ["z3"], out_shared_libs = select({ - "@platforms//os:linux": ["libz3.so"], - # "@platforms//os:osx": ["libz3.dylib"], # FIXME: this is not working, libz3.dylib is not copied + # NOTE: These will need to be manually bumped along side the version in MODULE.bazel/VERSION.txt/CMake + "@platforms//os:linux": [ + "libz3.so", + "libz3.so.4.17", + "libz3.so.4.17.0.0", + ], + "@platforms//os:osx": [ + "libz3.dylib", + "libz3.4.17.dylib", + "libz3.4.17.0.0.dylib", + ], "@platforms//os:windows": ["libz3.dll"], "//conditions:default": ["@platforms//:incompatible"], }), @@ -36,7 +45,7 @@ cmake( cmake( name = "z3_static", generate_args = [ - "-G Ninja", + "-G Ninja", "-D BUILD_SHARED_LIBS=OFF", "-D Z3_BUILD_LIBZ3_SHARED=OFF", ], diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ff592e0e9..491cb996b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,7 +204,12 @@ elseif (EMSCRIPTEN) "-Os" "-s ALLOW_MEMORY_GROWTH=1" "-s ASSERTIONS=0" - "-s DISABLE_EXCEPTION_CATCHING=0" + # Use native wasm exception handling + wasm longjmp to match the ABI of the + # Pyodide / modern-emscripten main module. The legacy JS-based EH (which the + # removed "-s DISABLE_EXCEPTION_CATCHING=0" selected) makes libz3 import + # invoke_* trampolines the Pyodide runtime no longer provides. + "-fwasm-exceptions" + "-s SUPPORT_LONGJMP=wasm" "-s ERROR_ON_UNDEFINED_SYMBOLS=1" ) endif() diff --git a/README.md b/README.md index 379c1c4fa2..ec8342772a 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ See the [release notes](RELEASE_NOTES.md) for notes on various stable releases o | [![WASM Build](https://github.com/Z3Prover/z3/actions/workflows/wasm.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/wasm.yml) | [![Windows](https://github.com/Z3Prover/z3/actions/workflows/Windows.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/Windows.yml) | [![CI](https://github.com/Z3Prover/z3/actions/workflows/ci.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/ci.yml) | [![OCaml Binding CI](https://github.com/Z3Prover/z3/actions/workflows/ocaml.yaml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/ocaml.yaml) | ### Scheduled Workflows -| Open Bugs | Android Build | Pyodide Build | Nightly Build | Cross Build | +| Open Bugs | Android Build | Pyodide Wheel (PyPI) | Nightly Build | Cross Build | | -----------|---------------|---------------|---------------|-------------| -| [![Open Issues](https://github.com/Z3Prover/z3/actions/workflows/wip.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/wip.yml) | [![Android Build](https://github.com/Z3Prover/z3/actions/workflows/android-build.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/android-build.yml) | [![Pyodide Build](https://github.com/Z3Prover/z3/actions/workflows/pyodide.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/pyodide.yml) | [![Nightly Build](https://github.com/Z3Prover/z3/actions/workflows/nightly.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/nightly.yml) | [![RISC V and PowerPC 64](https://github.com/Z3Prover/z3/actions/workflows/cross-build.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/cross-build.yml) | +| [![Open Issues](https://github.com/Z3Prover/z3/actions/workflows/wip.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/wip.yml) | [![Android Build](https://github.com/Z3Prover/z3/actions/workflows/android-build.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/android-build.yml) | [![Pyodide Wheel (PyPI)](https://github.com/Z3Prover/z3/actions/workflows/pyodide-pypi.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/pyodide-pypi.yml) | [![Nightly Build](https://github.com/Z3Prover/z3/actions/workflows/nightly.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/nightly.yml) | [![RISC V and PowerPC 64](https://github.com/Z3Prover/z3/actions/workflows/cross-build.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/cross-build.yml) | -| MSVC Static | MSVC Clang-CL | Build Z3 Cache | Code Coverage | Memory Safety | Mark PRs Ready | -|-------------|---------------|----------------|---------------|---------------|----------------| -| [![MSVC Static Build](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build.yml) | [![MSVC Clang-CL Static Build](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build-clang-cl.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build-clang-cl.yml) | [![Build and Cache Z3](https://github.com/Z3Prover/z3/actions/workflows/build-z3-cache.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/build-z3-cache.yml) | [![Code Coverage](https://github.com/Z3Prover/z3/actions/workflows/coverage.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/coverage.yml) | [![Memory Safety Analysis](https://github.com/Z3Prover/z3/actions/workflows/memory-safety.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/memory-safety.yml) | [![Mark PRs Ready for Review](https://github.com/Z3Prover/z3/actions/workflows/mark-prs-ready-for-review.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/mark-prs-ready-for-review.yml) | +| MSVC Static | MSVC Clang-CL | Build Z3 Cache | Memory Safety | Mark PRs Ready | +|-------------|---------------|----------------|---------------|----------------| +| [![MSVC Static Build](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build.yml) | [![MSVC Clang-CL Static Build](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build-clang-cl.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/msvc-static-build-clang-cl.yml) | [![Build and Cache Z3](https://github.com/Z3Prover/z3/actions/workflows/build-z3-cache.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/build-z3-cache.yml) | [![Memory Safety Analysis](https://github.com/Z3Prover/z3/actions/workflows/memory-safety.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/memory-safety.yml) | [![Mark PRs Ready for Review](https://github.com/Z3Prover/z3/actions/workflows/mark-prs-ready-for-review.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/mark-prs-ready-for-review.yml) | ### Manual & Release Workflows | Documentation | Release Build | WASM Release | NuGet Build | @@ -42,17 +42,17 @@ See the [release notes](RELEASE_NOTES.md) for notes on various stable releases o | [![Nightly Build Validation](https://github.com/Z3Prover/z3/actions/workflows/nightly-validation.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/nightly-validation.yml) | [![Copilot Setup Steps](https://github.com/Z3Prover/z3/actions/workflows/copilot-setup-steps.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/copilot-setup-steps.yml) | [![Agentics Maintenance](https://github.com/Z3Prover/z3/actions/workflows/agentics-maintenance.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/agentics-maintenance.yml) | ### Agentic Workflows -| A3 Python | API Coherence | Code Simplifier | Release Notes | Workflow Suggestion | -| ----------|---------------|-----------------|---------------|---------------------| -| [![A3 Python Code Analysis](https://github.com/Z3Prover/z3/actions/workflows/a3-python.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/a3-python.lock.yml) | [![API Coherence Checker](https://github.com/Z3Prover/z3/actions/workflows/api-coherence-checker.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/api-coherence-checker.lock.yml) | [![Code Simplifier](https://github.com/Z3Prover/z3/actions/workflows/code-simplifier.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/code-simplifier.lock.yml) | [![Release Notes Updater](https://github.com/Z3Prover/z3/actions/workflows/release-notes-updater.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/release-notes-updater.lock.yml) | [![Workflow Suggestion Agent](https://github.com/Z3Prover/z3/actions/workflows/workflow-suggestion-agent.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/workflow-suggestion-agent.lock.yml) | +| API Coherence | Code Simplifier | Release Notes | Workflow Suggestion | Academic Citation | +| -------------|-----------------|---------------|---------------------|-------------------| +| [![API Coherence Checker](https://github.com/Z3Prover/z3/actions/workflows/api-coherence-checker.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/api-coherence-checker.lock.yml) | [![Code Simplifier](https://github.com/Z3Prover/z3/actions/workflows/code-simplifier.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/code-simplifier.lock.yml) | [![Release Notes Updater](https://github.com/Z3Prover/z3/actions/workflows/release-notes-updater.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/release-notes-updater.lock.yml) | [![Workflow Suggestion Agent](https://github.com/Z3Prover/z3/actions/workflows/workflow-suggestion-agent.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/workflow-suggestion-agent.lock.yml) | [![Academic Citation Tracker](https://github.com/Z3Prover/z3/actions/workflows/academic-citation-tracker.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/academic-citation-tracker.lock.yml) | -| Academic Citation | Build Warning Fixer | Code Conventions | CSA Report | Issue Backlog | -| ------------------|---------------------|------------------|------------|---------------| -| [![Academic Citation Tracker](https://github.com/Z3Prover/z3/actions/workflows/academic-citation-tracker.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/academic-citation-tracker.lock.yml) | [![Build Warning Fixer](https://github.com/Z3Prover/z3/actions/workflows/build-warning-fixer.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/build-warning-fixer.lock.yml) | [![Code Conventions Analyzer](https://github.com/Z3Prover/z3/actions/workflows/code-conventions-analyzer.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/code-conventions-analyzer.lock.yml) | [![Clang Static Analyzer Report](https://github.com/Z3Prover/z3/actions/workflows/csa-analysis.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/csa-analysis.lock.yml) | [![Issue Backlog Processor](https://github.com/Z3Prover/z3/actions/workflows/issue-backlog-processor.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/issue-backlog-processor.lock.yml) | +| Issue Backlog | Memory Safety Report | QF-S Benchmark | Specbot Crash Analyzer | SMTLIB Benchmark Finder | +| --------------|----------------------|----------------|------------------------|-------------------------| +| [![Issue Backlog Processor](https://github.com/Z3Prover/z3/actions/workflows/issue-backlog-processor.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/issue-backlog-processor.lock.yml) | [![Memory Safety Report](https://github.com/Z3Prover/z3/actions/workflows/memory-safety-report.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/memory-safety-report.lock.yml) | [![ZIPT String Solver Benchmark](https://github.com/Z3Prover/z3/actions/workflows/qf-s-benchmark.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/qf-s-benchmark.lock.yml) | [![Specbot Crash Analyzer](https://github.com/Z3Prover/z3/actions/workflows/specbot-crash-analyzer.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/specbot-crash-analyzer.lock.yml) | [![SMTLIB Benchmark Finder](https://github.com/Z3Prover/z3/actions/workflows/smtlib-benchmark-finder.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/smtlib-benchmark-finder.lock.yml) | -| Memory Safety Report | Ostrich Benchmark | QF-S Benchmark | Tactic-to-Simplifier | ZIPT Code Reviewer | -| ---------------------|-------------------|----------------|----------------------|--------------------| -| [![Memory Safety Report](https://github.com/Z3Prover/z3/actions/workflows/memory-safety-report.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/memory-safety-report.lock.yml) | [![Ostrich Benchmark](https://github.com/Z3Prover/z3/actions/workflows/ostrich-benchmark.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/ostrich-benchmark.lock.yml) | [![ZIPT String Solver Benchmark](https://github.com/Z3Prover/z3/actions/workflows/qf-s-benchmark.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/qf-s-benchmark.lock.yml) | [![Tactic-to-Simplifier](https://github.com/Z3Prover/z3/actions/workflows/tactic-to-simplifier.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/tactic-to-simplifier.lock.yml) | [![ZIPT Code Reviewer](https://github.com/Z3Prover/z3/actions/workflows/zipt-code-reviewer.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/zipt-code-reviewer.lock.yml) | +| TPTP Benchmark | +|----------------| +| [![TPTP Front-End Benchmark](https://github.com/Z3Prover/z3/actions/workflows/tptp-benchmark.lock.yml/badge.svg)](https://github.com/Z3Prover/z3/actions/workflows/tptp-benchmark.lock.yml) | [1]: #building-z3-on-windows-using-visual-studio-command-prompt [2]: #building-z3-using-make-and-gccclang @@ -299,4 +299,3 @@ to Z3's C API. For more information, see [MachineArithmetic/README.md](https://g ## Power Tools * The [Axiom Profiler](https://github.com/viperproject/axiom-profiler-2) currently developed by ETH Zurich - diff --git a/cmake/check_link_atomic.cmake b/cmake/check_link_atomic.cmake index d462191a0b..aef5cd63fe 100644 --- a/cmake/check_link_atomic.cmake +++ b/cmake/check_link_atomic.cmake @@ -1,3 +1,8 @@ +if (EMSCRIPTEN OR Z3_SINGLE_THREADED) + # Emscripten (Pyodide/WASM) and single-threaded builds do not use + # libatomic; skip the check to avoid a spurious configure failure. + message(STATUS "Skipping std::atomic link check (EMSCRIPTEN or Z3_SINGLE_THREADED)") +else() set(ATOMIC_TEST_SOURCE " #include std::atomic x; @@ -21,3 +26,4 @@ if (NOT BUILTIN_ATOMIC) message(FATAL_ERROR "Host compiler must support std::atomic!") endif() endif() +endif() diff --git a/cmake/compiler_warnings.cmake b/cmake/compiler_warnings.cmake index d8dc90c055..2708725875 100644 --- a/cmake/compiler_warnings.cmake +++ b/cmake/compiler_warnings.cmake @@ -20,6 +20,11 @@ set(CLANG_ONLY_WARNINGS "-Wno-c++98-compat" "-Wno-c++98-compat-pedantic" "-Wno-zero-length-array" + "-Wc99-extensions" + "-Wsuggest-override" + "-Winconsistent-missing-override" + "-Wno-missing-field-initializers" + "-Wcast-qual" ) set(MSVC_WARNINGS "/W3") diff --git a/gmon.out b/gmon.out deleted file mode 100644 index cba10a19b2..0000000000 Binary files a/gmon.out and /dev/null differ diff --git a/scripts/mk_util.py b/scripts/mk_util.py index ff04bfb68c..89d82303a1 100644 --- a/scripts/mk_util.py +++ b/scripts/mk_util.py @@ -1804,6 +1804,7 @@ class DotNetDLLComponent(Component): Z3 is a satisfiability modulo theories solver from Microsoft Research. Copyright Microsoft Corporation. All rights reserved. smt constraint solver theorem prover + AnyCPU %s diff --git a/src/ackermannization/ackr_helper.cpp b/src/ackermannization/ackr_helper.cpp index 079782b312..7fecc3d8c5 100644 --- a/src/ackermannization/ackr_helper.cpp +++ b/src/ackermannization/ackr_helper.cpp @@ -18,13 +18,13 @@ double ackr_helper::calculate_lemma_bound(fun2terms_map const& occs1, sel2terms_map const& occs2) { double total = 0; - for (auto const& kv : occs1) { - total += n_choose_2_chk(kv.m_value->var_args.size()); - total += kv.m_value->const_args.size() * kv.m_value->var_args.size(); + for (auto const &[k, v] : occs1) { + total += n_choose_2_chk(v->var_args.size()); + total += v->const_args.size() * v->var_args.size(); } - for (auto const& kv : occs2) { - total += n_choose_2_chk(kv.m_value->var_args.size()); - total += kv.m_value->const_args.size() * kv.m_value->var_args.size(); + for (auto const &[k, v] : occs2) { + total += n_choose_2_chk(v->var_args.size()); + total += v->const_args.size() * v->var_args.size(); } return total; } diff --git a/src/ackermannization/ackr_helper.h b/src/ackermannization/ackr_helper.h index 5499e7d3a0..9c7c5fc1fc 100644 --- a/src/ackermannization/ackr_helper.h +++ b/src/ackermannization/ackr_helper.h @@ -52,14 +52,26 @@ public: return m_autil.is_select(a) && is_uninterp_const(a->get_arg(0)); } + void mark_non_select_rec(expr* t, expr_mark& visited, expr_mark& non_select) { + if (visited.is_marked(t)) + return; + visited.mark(t, true); + non_select.mark(t, true); + if (is_app(t)) { + for (expr *arg : *to_app(t)) + mark_non_select_rec(arg, visited,non_select); + } + } + void mark_non_select(app* a, expr_mark& non_select) { if (m_autil.is_select(a)) { bool first = true; + expr_mark visited; for (expr* arg : *a) { if (first) first = false; else - non_select.mark(arg, true); + mark_non_select_rec(arg, visited, non_select); } } else { @@ -70,10 +82,10 @@ public: void prune_non_select(obj_map & sels, expr_mark& non_select) { ptr_vector nons; - for (auto& kv : sels) { - if (non_select.is_marked(kv.m_key)) { - nons.push_back(kv.m_key); - dealloc(kv.m_value); + for (auto &[k, v] : sels) { + if (non_select.is_marked(k)) { + nons.push_back(k); + dealloc(v); } } for (app* s : nons) { diff --git a/src/ackermannization/lackr.cpp b/src/ackermannization/lackr.cpp index b438302b65..04e91970b8 100644 --- a/src/ackermannization/lackr.cpp +++ b/src/ackermannization/lackr.cpp @@ -149,9 +149,9 @@ void lackr::eager_enc() { checkpoint(); ackr(v); } - for (auto const& kv : m_sel2terms) { + for (auto const &[k, v] : m_sel2terms) { checkpoint(); - ackr(kv.get_value()); + ackr(v); } } @@ -190,13 +190,13 @@ void lackr::abstract_fun(fun2terms_map const& apps) { } void lackr::abstract_sel(sel2terms_map const& apps) { - for (auto const& kv : apps) { - func_decl * fd = kv.m_key->get_decl(); - for (app * t : kv.m_value->const_args) { + for (auto const &[k, v] : apps) { + func_decl * fd = k->get_decl(); + for (app * t : v->const_args) { app * fc = m.mk_fresh_const(fd->get_name(), t->get_sort()); m_info->set_abstr(t, fc); } - for (app * t : kv.m_value->var_args) { + for (app * t : v->var_args) { app * fc = m.mk_fresh_const(fd->get_name(), t->get_sort()); m_info->set_abstr(t, fc); } diff --git a/src/api/dotnet/Microsoft.Z3.csproj.in b/src/api/dotnet/Microsoft.Z3.csproj.in index cf5aacf464..55cb100be3 100644 --- a/src/api/dotnet/Microsoft.Z3.csproj.in +++ b/src/api/dotnet/Microsoft.Z3.csproj.in @@ -60,6 +60,7 @@ 4 true $(OutputPath)\Microsoft.Z3.xml + AnyCPU diff --git a/src/api/go/README.md b/src/api/go/README.md index 8814b72a1e..e499cea244 100644 --- a/src/api/go/README.md +++ b/src/api/go/README.md @@ -310,7 +310,7 @@ go run basic_example.go ## Memory Management -The Go bindings use `runtime.SetFinalizer` to automatically manage Z3 reference counts. You don't need to manually call inc_ref/dec_ref. However, be aware that finalizers run during garbage collection, so resources may not be freed immediately. +The Go bindings use `runtime.SetFinalizer` to automatically manage Z3 reference counts. You don't need to manually call inc_ref/dec_ref. However, be aware that finalizers run during garbage collection, so resources may not be freed immediately. The bindings enable `Z3_enable_concurrent_dec_ref` when creating contexts so finalizer-driven decref operations are safe with concurrent GC. ## Thread Safety diff --git a/src/api/go/z3.go b/src/api/go/z3.go index 4e982111e3..7c37b31332 100644 --- a/src/api/go/z3.go +++ b/src/api/go/z3.go @@ -89,6 +89,7 @@ type Context struct { // NewContext creates a new Z3 context with default configuration. func NewContext() *Context { ctx := &Context{ptr: C.Z3_mk_context_rc(C.Z3_mk_config())} + C.Z3_enable_concurrent_dec_ref(ctx.ptr) runtime.SetFinalizer(ctx, func(c *Context) { C.Z3_del_context(c.ptr) }) @@ -98,6 +99,7 @@ func NewContext() *Context { // NewContextWithConfig creates a new Z3 context with the given configuration. func NewContextWithConfig(cfg *Config) *Context { ctx := &Context{ptr: C.Z3_mk_context_rc(cfg.ptr)} + C.Z3_enable_concurrent_dec_ref(ctx.ptr) runtime.SetFinalizer(ctx, func(c *Context) { C.Z3_del_context(c.ptr) }) diff --git a/src/api/java/NativeStatic.txt b/src/api/java/NativeStatic.txt index 2266433609..c8bd267b5c 100644 --- a/src/api/java/NativeStatic.txt +++ b/src/api/java/NativeStatic.txt @@ -154,7 +154,7 @@ static void decide_eh(void* _p, Z3_solver_callback cb, Z3_ast _val, unsigned bit info->jenv->CallVoidMethod(info->jobj, info->decide, (jlong)_val, bit, is_pos); } -static jboolean on_binding_eh(void* _p, Z3_solver_callback cb, Z3_ast _q, Z3_ast _inst) { +[[maybe_unused]] static jboolean on_binding_eh(void* _p, Z3_solver_callback cb, Z3_ast _q, Z3_ast _inst) { JavaInfo *info = static_cast(_p); ScopedCB scoped(info, cb); return info->jenv->CallBooleanMethod(info->jobj, info->on_binding, (jlong)_q, (jlong)_inst); diff --git a/src/api/js/package-lock.json b/src/api/js/package-lock.json index 5c11307f40..be36753542 100644 --- a/src/api/js/package-lock.json +++ b/src/api/js/package-lock.json @@ -5400,10 +5400,20 @@ "dev": true }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -5501,15 +5511,25 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" diff --git a/src/api/python/pyproject.toml b/src/api/python/pyproject.toml index 380744c872..52c6db5946 100644 --- a/src/api/python/pyproject.toml +++ b/src/api/python/pyproject.toml @@ -1,3 +1,29 @@ [build-system] requires = ["setuptools>=70"] build-backend = "setuptools.build_meta" + +# --- Pyodide / WebAssembly (PEP 783) build configuration --------------------- +# Consumed by pyodide-build (invoked directly or via `cibuildwheel --platform +# pyodide`). These flags are forwarded to the emscripten toolchain that compiles +# libz3 to wasm32. setup.py's IS_PYODIDE branch appends the same -fexceptions +# flags defensively, but declaring them here is what cibuildwheel relies on. +# Pyodide 314 / emscripten 5 builds its main module with *native wasm* +# exception handling and wasm longjmp (see Pyodide's Makefile.envs). Side +# modules like libz3.so MUST match that ABI: building with the legacy +# `-fexceptions` (JS-based EH) makes libz3 import `invoke_*` trampolines that the +# Pyodide runtime no longer provides -> "cannot resolve symbol invoke_vi" at the +# first Z3 call. WASM_BIGINT is required because the Z3 C API passes 64-bit ints +# across the ctypes/JS boundary. +[tool.pyodide.build] +cflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm" +cxxflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm" +ldflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_BIGINT -sSIDE_MODULE=1" + +# --- cibuildwheel: produce a PyPI-publishable pyemscripten wheel ------------- +[tool.cibuildwheel] +# Pyodide 314 ships CPython 3.14; match the single ABI it targets. +build = "cp314-*" + +[tool.cibuildwheel.pyodide] +# z3test.py is the upstream smoke test; run it inside the Pyodide test venv. +test-command = "python {project}/z3test.py z3" diff --git a/src/api/python/setup.py b/src/api/python/setup.py index 116b3b570e..b199effd39 100644 --- a/src/api/python/setup.py +++ b/src/api/python/setup.py @@ -42,9 +42,16 @@ if RELEASE_DIR is None: BUILD_PLATFORM = "emscripten" BUILD_ARCH = "wasm32" BUILD_OS_VERSION = os.environ['_PYTHON_HOST_PLATFORM'].split('_')[1:-1] - build_env['CFLAGS'] = build_env.get('CFLAGS', '') + " -fexceptions" - build_env['CXXFLAGS'] = build_env.get('CXXFLAGS', '') + " -fexceptions" - build_env['LDFLAGS'] = build_env.get('LDFLAGS', '') + " -fexceptions" + # Match Pyodide's native-wasm exception/longjmp ABI (see Makefile.envs in + # Pyodide). The legacy JS-based "-fexceptions" makes libz3.so import + # invoke_* trampolines that the modern Pyodide runtime does not export, + # which surfaces as "cannot resolve symbol invoke_vi" on the first Z3 + # call. These mirror [tool.pyodide.build] in pyproject.toml so direct + # `pyodide build` invocations stay consistent with cibuildwheel. + _wasm_eh = " -fwasm-exceptions -sSUPPORT_LONGJMP=wasm" + build_env['CFLAGS'] = build_env.get('CFLAGS', '') + _wasm_eh + build_env['CXXFLAGS'] = build_env.get('CXXFLAGS', '') + _wasm_eh + build_env['LDFLAGS'] = build_env.get('LDFLAGS', '') + _wasm_eh + " -sWASM_BIGINT -sSIDE_MODULE=1" IS_SINGLE_THREADED = True ENABLE_LTO = False # build with pthread doesn't work. The WASM bindings are also single threaded. @@ -305,6 +312,21 @@ class bdist_wheel(_bdist_wheel): def finalize_options(self): + if BUILD_PLATFORM == "emscripten": + # Under pyodide-build / `cibuildwheel --platform pyodide`, the + # authoritative wheel platform tag is handed to us verbatim via + # _PYTHON_HOST_PLATFORM. For PEP 783 (Pyodide >= 0.28 / "314") this + # is e.g. "pyemscripten_2026_0_wasm32" -- a tag PyPI accepts. The + # reconstruction below instead produced "emscripten__wasm32", + # which is locked to a Pyodide release and rejected by PyPI, so we + # defer to pyodide-build's tag when it is available. + host_platform = os.environ.get('_PYTHON_HOST_PLATFORM') + if host_platform: + self.plat_name = host_platform + else: + os_version_tag = '_'.join(BUILD_OS_VERSION) if BUILD_OS_VERSION else 'xxxxxx' + self.plat_name = f"emscripten_{os_version_tag}_wasm32" + return super().finalize_options() if BUILD_ARCH is not None and BUILD_PLATFORM is not None: os_version_tag = '_'.join(BUILD_OS_VERSION) if BUILD_OS_VERSION is not None else 'xxxxxx' os_version_tag = self.remove_build_machine_os_version(BUILD_PLATFORM, os_version_tag) diff --git a/src/api/z3_replayer.cpp b/src/api/z3_replayer.cpp index f044cb1421..a8315b073b 100644 --- a/src/api/z3_replayer.cpp +++ b/src/api/z3_replayer.cpp @@ -621,7 +621,7 @@ struct z3_replayer::imp { Z3_symbol get_symbol(unsigned pos) const { check_arg(pos, SYMBOL); - return (Z3_symbol)m_args[pos].m_sym; + return (Z3_symbol)const_cast(m_args[pos].m_sym); } void * get_obj(unsigned pos) const { diff --git a/src/ast/ast.cpp b/src/ast/ast.cpp index 928bebc078..b0fa217d62 100644 --- a/src/ast/ast.cpp +++ b/src/ast/ast.cpp @@ -2894,7 +2894,7 @@ proof * ast_manager::mk_transitivity(unsigned num_proofs, proof * const * proofs } }); ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(mk_eq(n1,n2)); return mk_app(basic_family_id, PR_TRANSITIVITY_STAR, args.size(), args.data()); } @@ -2903,7 +2903,7 @@ proof * ast_manager::mk_monotonicity(func_decl * R, app * f1, app * f2, unsigned SASSERT(f1->get_num_args() == f2->get_num_args()); SASSERT(f1->get_decl() == f2->get_decl()); ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(mk_app(R, f1, f2)); proof* p = mk_app(basic_family_id, PR_MONOTONICITY, args.size(), args.data()); return p; @@ -2965,7 +2965,7 @@ proof * ast_manager::mk_rewrite_star(expr * s, expr * t, unsigned num_proofs, pr if (proofs_disabled()) return nullptr; ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(mk_eq(s, t)); return mk_app(basic_family_id, PR_REWRITE_STAR, args.size(), args.data()); } @@ -3055,7 +3055,7 @@ proof * ast_manager::mk_unit_resolution(unsigned num_proofs, proof * const * pro } if (!found_complement) { - args.append(num_proofs, (expr**)proofs); + args.append(num_proofs, (expr* const *)proofs); CTRACE(mk_unit_resolution_bug, !is_or(f1), tout << mk_ll_pp(f1, *this) << "\n"; for (unsigned i = 1; i < num_proofs; ++i) tout << mk_pp(proofs[i], *this) << "\n"; @@ -3125,7 +3125,7 @@ proof * ast_manager::mk_unit_resolution(unsigned num_proofs, proof * const * pro tout << mk_pp(new_fact, *this) << "\n";); ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(new_fact); #ifdef Z3DEBUG expr * f1 = get_fact(proofs[0]); @@ -3191,7 +3191,7 @@ proof * ast_manager::mk_apply_defs(expr * n, expr * def, unsigned num_proofs, pr if (proofs_disabled()) return nullptr; ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(mk_oeq(n, def)); return mk_app(basic_family_id, PR_APPLY_DEF, args.size(), args.data()); } @@ -3225,7 +3225,7 @@ proof * ast_manager::mk_nnf_pos(expr * s, expr * t, unsigned num_proofs, proof * return nullptr; check_nnf_proof_parents(num_proofs, proofs); ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(mk_oeq(s, t)); return mk_app(basic_family_id, PR_NNF_POS, args.size(), args.data()); } @@ -3235,7 +3235,7 @@ proof * ast_manager::mk_nnf_neg(expr * s, expr * t, unsigned num_proofs, proof * return nullptr; check_nnf_proof_parents(num_proofs, proofs); ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(mk_oeq(mk_not(s), t)); return mk_app(basic_family_id, PR_NNF_NEG, args.size(), args.data()); } @@ -3305,7 +3305,7 @@ proof * ast_manager::mk_redundant_del(expr* e) { proof * ast_manager::mk_clause_trail(unsigned n, expr* const* ps) { ptr_buffer args; - args.append(n, (expr**) ps); + args.append(n, (expr* const *) ps); return mk_app(basic_family_id, PR_CLAUSE_TRAIL, 0, nullptr, args.size(), args.data()); } @@ -3323,7 +3323,7 @@ proof * ast_manager::mk_th_lemma( for (unsigned i = 0; i < num_params; ++i) parameters.push_back(params[i]); ptr_buffer args; - args.append(num_proofs, (expr**) proofs); + args.append(num_proofs, (expr* const *) proofs); args.push_back(fact); return mk_app(basic_family_id, PR_TH_LEMMA, parameters.size(), parameters.data(), args.size(), args.data()); } diff --git a/src/ast/ast_pp_dot.cpp b/src/ast/ast_pp_dot.cpp index 17750a7b9a..c323a0366d 100644 --- a/src/ast/ast_pp_dot.cpp +++ b/src/ast/ast_pp_dot.cpp @@ -68,8 +68,8 @@ private: inline ast_manager & m() const { return m_manager; } // label for an expression - std::string label_of_expr(const expr * e) const { - expr_ref er((expr*)e, m()); + std::string label_of_expr(const expr *e) const { + expr_ref er(const_cast(e), m()); std::ostringstream out; out << er << std::flush; return escape_dot(out.str()); diff --git a/src/ast/rewriter/CMakeLists.txt b/src/ast/rewriter/CMakeLists.txt index be8449978d..df06cedfe3 100644 --- a/src/ast/rewriter/CMakeLists.txt +++ b/src/ast/rewriter/CMakeLists.txt @@ -39,11 +39,16 @@ z3_add_component(rewriter rewriter.cpp seq_axioms.cpp seq_eq_solver.cpp + seq_derive.cpp seq_subset.cpp seq_split.cpp + seq_derive.cpp + seq_range_collapse.cpp + seq_range_predicate.cpp seq_rewriter.cpp seq_regex_bisim.cpp seq_skolem.cpp + term_enumeration.cpp th_rewriter.cpp value_sweep.cpp var_subst.cpp diff --git a/src/ast/rewriter/bit_blaster/bit_blaster_tpl_def.h b/src/ast/rewriter/bit_blaster/bit_blaster_tpl_def.h index 342ff5ed8b..87785d8f82 100644 --- a/src/ast/rewriter/bit_blaster/bit_blaster_tpl_def.h +++ b/src/ast/rewriter/bit_blaster/bit_blaster_tpl_def.h @@ -768,9 +768,10 @@ void bit_blaster_tpl::mk_smod(unsigned sz, expr * const * a_bits, expr * co template void bit_blaster_tpl::mk_eq(unsigned sz, expr * const * a_bits, expr * const * b_bits, expr_ref & out) { expr_ref_vector out_bits(m()); + out_bits.resize(sz); for (unsigned i = 0; i < sz; ++i) { mk_iff(a_bits[i], b_bits[i], out); - out_bits.push_back(out); + out_bits[i] = out; } mk_and(out_bits.size(), out_bits.data(), out); } diff --git a/src/ast/rewriter/bool_rewriter.cpp b/src/ast/rewriter/bool_rewriter.cpp index 86c5524987..945aa297ee 100644 --- a/src/ast/rewriter/bool_rewriter.cpp +++ b/src/ast/rewriter/bool_rewriter.cpp @@ -1196,15 +1196,15 @@ bool bool_rewriter::decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref & } for (expr *e : subterms::ground(expr_ref(r, m()))) { if (m().is_ite(e, cond, r1, r2)) { - expr_safe_replace rep1(m()); - expr_safe_replace rep2(m()); - rep1.insert(e, r1); - rep2.insert(e, r2); + m_rep1.reset(); + m_rep2.reset(); + m_rep1.insert(e, r1); + m_rep2.insert(e, r2); c = cond; th = r; el = r; - rep1(th); - rep2(el); + m_rep1(th); + m_rep2(el); return true; } } @@ -1212,6 +1212,4 @@ bool bool_rewriter::decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref & } - - -template class rewriter_tpl; +template class rewriter_tpl; \ No newline at end of file diff --git a/src/ast/rewriter/bool_rewriter.h b/src/ast/rewriter/bool_rewriter.h index 56c0b3e77a..87c50f171e 100644 --- a/src/ast/rewriter/bool_rewriter.h +++ b/src/ast/rewriter/bool_rewriter.h @@ -20,6 +20,7 @@ Notes: #include "ast/ast.h" #include "ast/rewriter/rewriter.h" +#include "ast/rewriter/expr_safe_replace.h" #include "util/params.h" /** @@ -64,6 +65,7 @@ class bool_rewriter { ptr_vector m_todo1, m_todo2; unsigned_vector m_counts1, m_counts2; expr_mark m_marked; + expr_safe_replace m_rep1, m_rep2; br_status mk_flat_and_core(unsigned num_args, expr * const * args, expr_ref & result); br_status mk_flat_or_core(unsigned num_args, expr * const * args, expr_ref & result); @@ -87,7 +89,7 @@ class bool_rewriter { expr_ref simplify_eq_ite(expr* value, expr* ite); public: - bool_rewriter(ast_manager & m, params_ref const & p = params_ref()):m_manager(m), m_local_ctx_cost(0) { + bool_rewriter(ast_manager & m, params_ref const & p = params_ref()):m_manager(m), m_local_ctx_cost(0), m_rep1(m), m_rep2(m) { updt_params(p); } ast_manager & m() const { return m_manager; } @@ -243,7 +245,9 @@ public: void mk_nor(expr * arg1, expr * arg2, expr_ref & result); void mk_ge2(expr* a, expr* b, expr* c, expr_ref& result); - + // If r is, or contains, an if-then-else, decompose it into a top-level + // ite by hoisting the (first) inner ite condition: returns c, th, el such + // that r is equivalent to (ite c th el). Returns false if r has no ite. bool decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref &el); }; diff --git a/src/ast/rewriter/seq_axioms.cpp b/src/ast/rewriter/seq_axioms.cpp index e3ee8b4369..c309afa59b 100644 --- a/src/ast/rewriter/seq_axioms.cpp +++ b/src/ast/rewriter/seq_axioms.cpp @@ -452,6 +452,8 @@ namespace seq { // |t| = 0 => |s| = 0 or indexof(t,s,offset) = -1 // ~contains(t,s) => indexof(t,s,offset) = -1 + add_clause(mk_ge(i, -1)); + add_clause(cnt, i_eq_m1); add_clause(~t_eq_empty, s_eq_empty, i_eq_m1); @@ -638,8 +640,8 @@ namespace seq { add_clause(~i_ge_0, i_ge_len_s, mk_eq(i, len_x)); } - add_clause(i_ge_0, mk_seq_eq(e, emp)); - add_clause(~i_ge_len_s, mk_seq_eq(e, emp)); + add_clause(i_ge_0, mk_eq(e, emp)); + add_clause(~i_ge_len_s, mk_eq(e, emp)); add_clause(~i_ge_0, i_ge_len_s, mk_eq(one, len_e)); add_clause(mk_le(len_e, 1)); } @@ -1066,7 +1068,7 @@ namespace seq { void axioms::replace_re_axiom(expr* e) { expr* s = nullptr, *r = nullptr, *t = nullptr; VERIFY(seq.str.is_replace_re(e, s, r, t)); - throw default_exception("replace-re is not supported"); + throw default_exception("no support for replace-re"); } // A basic strategy for supporting replace_all and other @@ -1075,34 +1077,22 @@ namespace seq { // using iterative deepening can be re-used. // // create recursive relation 'ra' with properties: - // ra(i, j, s, p, t, r) = - // if len(s) = i && len(r) = j then - // true - // else if len(s) > i = 0 && p = "" && r = t + s then - // true - // else if len(s) > i && p != "" && - // s = extract(s, 0, i) + p + extract(s, i + len(p), len(s)) && - // r = extract(r, 0, i) + t + extract(r, i + len(p), len(r)) && ra(i + len(p), j + len(t), s, p, t, r) - // else if ~prefix(p, extract(s, i, len(s)) && at(s,i) = at(r,j) then - // ra(i + 1, j + 1, s, p, t, r) - // else false + // ra(i, j, s, p, t, r) <- len(s) = i && len(r) = j + // ra(i, j, s, p, t, r) <- len(s) > i = 0 && p = "" && r = t + s + // ra(i, j, s, p, t, r) <- len(s) > i && p != "" && s = extract(s, 0, i) + p + extract(s, i + len(p), len(s)) && r = extract(r, 0, i) + t + extract(r, i + len(p), len(r)) && ra(i + len(p), j + len(t), s, p, t, r) + // ra(i, s, p, t, r) <- ~prefix(p, extract(s, i, len(s)) && at(s,i) = at(r,j) && ra(i + 1, j + 1, s, p, t, r) + // which amounts to: + // + // // Then assert // ra(s, p, t, replace_all(s, p, t)) // - // ra(s, p, t, r) is a recursive predicate: - // ra(s, p, t, r) iff replace_all(s, p, t) = r - // - // Base case, empty s or p: r = s - // Match case, prefix(p, s): s = p ++ s', r = t ++ r', ra(s', p, t, r') - // No-match case: r[0] = s[0], ra(s[1:], p, t, r[1:]) - // - // Assert: ra(s, p, t, replace_all(s, p, t)) - // void axioms::replace_all_axiom(expr* r) { expr* s = nullptr, *p = nullptr, *t = nullptr; VERIFY(seq.str.is_replace_all(r, s, p, t)); recfun::util rec(m); recfun::decl::plugin& plugin = rec.get_plugin(); + recfun_replace replace(m); sort* srt = s->get_sort(); sort* domain[4] = { srt, srt, srt, srt }; auto ra = rec.find_def_decl(symbol("ra"), 4, domain, m.mk_bool_sort(), true); @@ -1146,7 +1136,7 @@ namespace seq { void axioms::replace_re_all_axiom(expr* e) { expr* s = nullptr, *p = nullptr, *t = nullptr; VERIFY(seq.str.is_replace_re_all(e, s, p, t)); - throw default_exception("replace_re_all is not supported"); + throw default_exception("no support for replace-re-all"); } @@ -1345,7 +1335,7 @@ namespace seq { /** * Consider the recursive definition of negated contains: - ~contains(a, b) = + ~contains(a, b) = if |b| > |a| then true else if |b| = |a| then a != b else ~prefix(b, a) and ~contains(a[1:], b) @@ -1420,9 +1410,9 @@ namespace seq { return bound_tracker; } - // |u| != |v| OR + // |u| != |v| OR // (u = w[a]u' AND v = w[b]v' AND a != b AND |u'| = |v'|) - void axioms::diseq_axiom(expr *u, expr *v) { + void axioms::diseq_axiom(expr *u, expr *v) { expr_ref u_len(mk_len(u), m); expr_ref v_len(mk_len(v), m); expr_ref len_eq(mk_eq(u_len, v_len), m); diff --git a/src/ast/rewriter/seq_derive.cpp b/src/ast/rewriter/seq_derive.cpp new file mode 100644 index 0000000000..bd39e042a3 --- /dev/null +++ b/src/ast/rewriter/seq_derive.cpp @@ -0,0 +1,1520 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + seq_derive.cpp + +Abstract: + + Symbolic derivative computation for regular expressions. + Produces an ITE-tree (transition regex) representation following + the approach of RE# (Varatalu, Veanes, Ernits - POPL 2025). + + The symbolic derivative δ(r) maps each character to the resulting + derivative state via an ITE-tree. The free variable (:var 0) represents + the input character. + +Authors: + + Nikolaj Bjorner (nbjorner) 2026-06-03 + +--*/ + +#include "ast/rewriter/seq_derive.h" +#include "ast/rewriter/seq_rewriter.h" +#include "ast/rewriter/var_subst.h" +#include "ast/ast_pp.h" +#include "ast/array_decl_plugin.h" +#include "ast/rewriter/bool_rewriter.h" +#include "util/util.h" +#include + +namespace seq { + + derive::derive(ast_manager& m, seq_rewriter& re) : + m(m), + m_util(m), + m_autil(m), + m_br(m), + m_re(re), + m_trail(m), + m_ele(m), + m_path_expr(m) { + m_br.set_flat_and_or(false); + } + + void derive::reset() { + m_acache.reset(); + m_bcache.reset(); + m_atop_cache.reset(); + m_btop_cache.reset(); + reset_op_caches(); + m_trail.reset(); + m_ele = nullptr; + } + + // Reset only operation caches (union/inter/concat/complement) + // while preserving derivative caches (m_cache, m_top_cache) + // The op cache does index on m_ele so it has to be reset if m_ele changes. + void derive::reset_op_caches() { + m_aunion_cache.reset(); + m_ainter_cache.reset(); + m_aconcat_cache.reset(); + m_acomplement_cache.reset(); + m_bunion_cache.reset(); + m_binter_cache.reset(); + m_bconcat_cache.reset(); + m_bcomplement_cache.reset(); + m_ele = nullptr; + } + + expr_ref derive::operator()(derivative_kind k, expr* ele, expr* r) { + m_derivative_kind = k; + SASSERT(m_util.is_re(r)); + if (m_trail.size() > 500000) + reset(); + else if (m_trail.size() > 100000 || ele != m_ele) + reset_op_caches(); + sort *seq_sort = nullptr, *ele_sort = nullptr; + VERIFY(m_util.is_re(r, seq_sort)); + VERIFY(m_util.is_seq(seq_sort, ele_sort)); + // Check top-level cache (post-simplify result) + expr* cached = nullptr; + expr_ref result(m); + if (top_cache().find(ele, r, cached)) { + result = cached; + return result; + } + // Pin ele and r + m_trail.push_back(ele); + m_trail.push_back(r); + + // Always compute the SYMBOLIC derivative wrt the canonical + // variable v (so the cached result is reusable for any + // concrete ele via substitution below). Using the concrete + // `ele` here would bake it into the cached ITE-tree and + // poison future lookups for the same r with a different ele. + m_ele = ele; + m_depth = 0; + // Initialize path state for inline pruning + m_path.reset(); + m_intervals.reset(); + m_intervals.push_back({0u, u().max_char()}); + m_intervals_start = 0; + m_path_expr = m.mk_true(); + result = derive_rec(r); + top_cache().insert(ele, r, result); + + // pin the final result + m_trail.push_back(result); + return result; + } + + expr_ref derive::operator()(derivative_kind k, expr* r) { + SASSERT(m_util.is_re(r)); + sort* seq_sort = nullptr, * ele_sort = nullptr; + VERIFY(m_util.is_re(r, seq_sort)); + VERIFY(m_util.is_seq(seq_sort, ele_sort)); + expr_ref v(m.mk_var(0, ele_sort), m); + return (*this)(k,v, r); + } + + // ------------------------------------------------------- + // Core derivative computation + // ------------------------------------------------------- + + expr_ref derive::derive_rec(expr* r) { + SASSERT(m_util.is_re(r)); + + // Check cache (indexed by both m_ele and r) + expr* cached = nullptr; + if (cache().find(m_ele, r, cached)) + return expr_ref(cached, m); + + // Depth check + if (m_depth >= m_max_depth) { + // Return stuck derivative (the derivative operator applied symbolically) + return expr_ref(re().mk_derivative(m_ele, r), m); + } + + flet _scoped_depth(m_depth, m_depth + 1); + expr_ref result = derive_core(r); + + // Cache the result + cache().insert(m_ele, r, result); + m_trail.push_back(m_ele); + m_trail.push_back(r); + m_trail.push_back(result); + return result; + } + + // Forward declaration helper + expr_ref derive::derive_core(expr* r) { + sort* s = nullptr; + VERIFY(m_util.is_re(r, s)); + + auto nothing = [&]() { return expr_ref(re().mk_empty(r->get_sort()), m); }; + auto epsilon = [&]() { return expr_ref(re().mk_to_re(u().str.mk_empty(s)), m); }; + auto dotstar = [&]() { return expr_ref(re().mk_full_seq(r->get_sort()), m); }; + + expr* r1 = nullptr; + expr* r2 = nullptr; + expr* cond = nullptr; + unsigned lo = 0, hi = 0; + + // δ(∅) = ∅, δ(ε) = ∅ + if (re().is_empty(r) || re().is_epsilon(r)) + return nothing(); + + // δ(Σ*) = Σ*, δ(.+) = Σ* + if (re().is_full_seq(r) || re().is_dot_plus(r)) + return dotstar(); + + // δ(.) = ε (full char accepts any single character) + if (re().is_full_char(r)) + return epsilon(); + + // δ(str.to_re(s)) - derivative of a literal string + if (re().is_to_re(r, r1)) + return derive_to_re(r1, s); + + // δ(re.range(lo, hi)) - character range + if (re().is_range(r, r1, r2)) + return derive_range(r1, r2, s); + + // δ(re.of_pred(p)) - predicate-based regex + if (re().is_of_pred(r, r1)) + return derive_of_pred(r1, s); + + // δ(r1 · r2) = δ(r1) · r2 ∪ (if nullable(r1) then δ(r2) else ∅) + if (re().is_concat(r, r1, r2)) { + // Ensure right-associative form first. A left-nested concat + // (a·b)·r2 makes the head r1 a large sub-concat, so deriving it + // recurses through the whole left spine and can exceed + // m_max_depth, producing stuck symbolic re.derivative terms that + // accumulate across states and blow up. mk_concat right- + // associates in a single linear pass (without touching the + // derivative depth counter), keeping the head atomic. + if (re().is_concat(r1)) { + expr_ref rr = mk_concat(r1, r2); + if (rr != r) + return derive_rec(rr); + } + expr_ref d1 = derive_rec(r1); + expr_ref d1_r2 = mk_deriv_concat(d1, r2); + expr_ref nullable_r1 = is_nullable(r1); + if (m.is_true(nullable_r1)) + return mk_union(d1_r2, derive_rec(r2)); + if (m.is_false(nullable_r1)) + return d1_r2; + // Conditional: nullable is a Boolean expression + expr_ref d2 = derive_rec(r2); + expr_ref guarded = mk_ite(nullable_r1, d2, nothing()); + return mk_union(d1_r2, guarded); + } + + // δ(r1 ∪ r2) = δ(r1) ∪ δ(r2) + if (re().is_union(r, r1, r2)) { + expr_ref d1 = derive_rec(r1); + expr_ref d2 = derive_rec(r2); + return mk_union(d1, d2); + } + + // δ(r1 x r2) = δ(r1) x δ(r2) + if (re().is_xor(r, r1, r2)) { + expr_ref d1 = derive_rec(r1); + expr_ref d2 = derive_rec(r2); + return mk_xor(d1, d2); + } + + // δ(r1 ∩ r2) = δ(r1) ∩ δ(r2) + if (re().is_intersection(r, r1, r2)) { + expr_ref d1 = derive_rec(r1); + expr_ref d2 = derive_rec(r2); + return mk_inter(d1, d2); + } + + // δ(~r1) = ~δ(r1) + if (re().is_complement(r, r1)) { + expr_ref d1 = derive_rec(r1); + return mk_complement(d1); + } + + // δ(r1*) = δ(r1) · r1* + if (re().is_star(r, r1)) { + expr_ref d1 = derive_rec(r1); + expr_ref star_r1(re().mk_star(r1), m); + return mk_deriv_concat(d1, star_r1); + } + + // δ(r1+) = δ(r1) · r1* + if (re().is_plus(r, r1)) { + expr_ref d1 = derive_rec(r1); + expr_ref star_r1(re().mk_star(r1), m); + return mk_deriv_concat(d1, star_r1); + } + + // δ(r1?) = δ(r1) + if (re().is_opt(r, r1)) + return derive_rec(r1); + + // δ(r1{lo,hi}) + if (re().is_loop(r, r1, lo, hi)) { + if (hi == 0 || hi < lo) + return nothing(); + expr_ref d1 = derive_rec(r1); + expr_ref tail(re().mk_loop_proper(r1, (lo == 0 ? 0 : lo - 1), hi - 1), m); + return mk_deriv_concat(d1, tail); + } + + // δ(r1{lo,}) - unbounded loop + if (re().is_loop(r, r1, lo)) { + expr_ref d1 = derive_rec(r1); + expr_ref tail(re().mk_loop(r1, (lo == 0 ? 0 : lo - 1)), m); + return mk_deriv_concat(d1, tail); + } + + // δ(r1 \ r2) = δ(r1) ∩ ~δ(r2) + if (re().is_diff(r, r1, r2)) { + expr_ref d1 = derive_rec(r1); + expr_ref d2 = derive_rec(r2); + expr_ref neg_d2 = mk_complement(d2); + return mk_inter(d1, neg_d2); + } + + // δ(ite(c, r1, r2)) = ite(c, δ(r1), δ(r2)) + if (m.is_ite(r, cond, r1, r2)) { + expr_ref d1 = derive_rec(r1); + expr_ref d2 = derive_rec(r2); + return mk_ite(cond, d1, d2); + } + + // δ(reverse(r1)) - normalize by pushing reverse inward, then derive + if (re().is_reverse(r, r1)) { + expr_ref norm = mk_regex_reverse(r1); + if (norm != r) + return derive_rec(norm); + return expr_ref(re().mk_derivative(m_ele, r), m); + } + + // Stuck/uninterpreted case + return expr_ref(re().mk_derivative(m_ele, r), m); + } + + // ------------------------------------------------------- + // Derivative of specific regex constructs + // ------------------------------------------------------- + + expr_ref derive::derive_to_re(expr* s, sort* seq_sort) { + sort* re_sort = re().mk_re(seq_sort); + // δ(to_re("")) = ∅ + if (u().str.is_empty(s)) + return expr_ref(re().mk_empty(re_sort), m); + + // δ(to_re("c₁c₂...cₙ")) = ite(ele = c₁, to_re("c₂...cₙ"), ∅) + zstring zs; + if (u().str.is_string(s, zs)) { + if (zs.length() == 0) + return expr_ref(re().mk_empty(re_sort), m); + // First character + expr_ref head(m_util.mk_char(zs[0]), m); + expr_ref cond(m.mk_eq(m_ele, head), m); + // Tail string + expr_ref tail_str(u().str.mk_string(zs.extract(1, zs.length() - 1)), m); + expr_ref tail_re(re().mk_to_re(tail_str), m); + expr_ref empty(re().mk_empty(re_sort), m); + return mk_ite(cond, tail_re, empty); + } + + // δ(to_re(unit(c))) = ite(ele = c, ε, ∅) + expr* ch = nullptr; + if (u().str.is_unit(s, ch)) { + expr_ref eps(re().mk_to_re(u().str.mk_empty(seq_sort)), m); + expr_ref empty(re().mk_empty(re_sort), m); + expr_ref cond(m.mk_eq(m_ele, ch), m); + return mk_ite(cond, eps, empty); + } + + // δ(to_re(s1 ++ s2)) = ite(head matches, to_re(tail ++ s2), ∅) + expr* s1 = nullptr, * s2 = nullptr; + if (u().str.is_concat(s, s1, s2)) { + expr_ref hd(m), tl(m); + if (get_head_tail(s1, s2, hd, tl)) { + expr_ref cond(m.mk_eq(m_ele, hd), m); + expr_ref tail_re(re().mk_to_re(tl), m); + expr_ref empty(re().mk_empty(re_sort), m); + return mk_ite(cond, tail_re, empty); + } + } + + // δ(to_re(itos(n))) - derivative of integer-to-string + // itos(n) produces digits '0'-'9' when n >= 0, empty when n < 0 + expr* n = nullptr; + if (u().str.is_itos(s, n)) { + expr_ref empty(re().mk_empty(re_sort), m); + // Guard: n >= 0 and element is a digit and element = s[0] + expr_ref n_ge_0(m_autil.mk_ge(n, m_autil.mk_int(0)), m); + expr_ref char_0(m_util.mk_char('0'), m); + expr_ref char_9(m_util.mk_char('9'), m); + expr_ref ge_0(m_util.mk_le(char_0, m_ele), m); + expr_ref le_9(m_util.mk_le(m_ele, char_9), m); + expr_ref is_digit(m.mk_and(ge_0, le_9), m); + // First character of itos(n) matches ele + expr_ref zero_idx(m_autil.mk_int(0), m); + expr_ref first(u().str.mk_nth_i(s, zero_idx), m); + expr_ref eq_first(m.mk_eq(m_ele, first), m); + // Guard = n >= 0 && is_digit && ele = s[0] + expr_ref guard(m.mk_and(n_ge_0, m.mk_and(is_digit, eq_first)), m); + // Tail: to_re(substr(itos(n), 1, len(itos(n)) - 1)) + expr_ref one(m_autil.mk_int(1), m); + expr_ref len(u().str.mk_length(s), m); + expr_ref rest_len(m_autil.mk_sub(len, one), m); + expr_ref rest(u().str.mk_substr(s, one, rest_len), m); + expr_ref rest_re(re().mk_to_re(rest), m); + return mk_ite(guard, rest_re, empty); + } + + // Non-ground sequence: δ(to_re(s)) = ite(s ≠ "" ∧ ele = s[0], to_re(s[1:]), ∅) + expr_ref empty_seq(u().str.mk_empty(seq_sort), m); + expr_ref is_non_empty(m.mk_not(m.mk_eq(s, empty_seq)), m); + expr_ref zero(m_autil.mk_int(0), m); + expr_ref first(u().str.mk_nth_i(s, zero), m); + expr_ref eq_first(m.mk_eq(m_ele, first), m); + expr_ref guard(m.mk_and(is_non_empty, eq_first), m); + expr_ref one(m_autil.mk_int(1), m); + expr_ref len(u().str.mk_length(s), m); + expr_ref rest_len(m_autil.mk_sub(len, one), m); + expr_ref rest(u().str.mk_substr(s, one, rest_len), m); + expr_ref rest_re(re().mk_to_re(rest), m); + expr_ref empty(re().mk_empty(re_sort), m); + return mk_ite(guard, rest_re, empty); + } + + expr_ref derive::derive_range(expr* lo, expr* hi, sort* seq_sort) { + sort* re_sort = re().mk_re(seq_sort); + expr_ref empty(re().mk_empty(re_sort), m); + expr_ref eps(re().mk_to_re(u().str.mk_empty(seq_sort)), m); + + // Extract character values from unit strings + expr_ref c_lo(m), c_hi(m); + if (u().str.is_unit_string(lo, c_lo) && u().str.is_unit_string(hi, c_hi)) { + // Build range condition, simplifying trivial bounds + unsigned lo_val = 0, hi_val = 0; + bool lo_trivial = m_util.is_const_char(c_lo, lo_val) && lo_val == 0; + bool hi_trivial = m_util.is_const_char(c_hi, hi_val) && hi_val == u().max_char(); + + if (lo_trivial && hi_trivial) + return eps; // full charset range — always matches + + expr_ref in_range(m); + if (lo_trivial) + in_range = m_util.mk_le(m_ele, c_hi); + else if (hi_trivial) + in_range = m_util.mk_le(c_lo, m_ele); + else + in_range = m.mk_and(m_util.mk_le(c_lo, m_ele), m_util.mk_le(m_ele, c_hi)); + + return mk_ite(in_range, eps, empty); + } + + // Fallback: stuck derivative + return expr_ref(re().mk_derivative(m_ele, re().mk_range(lo, hi)), m); + } + + expr_ref derive::derive_of_pred(expr* pred, sort* seq_sort) { + sort* re_sort = re().mk_re(seq_sort); + expr_ref empty(re().mk_empty(re_sort), m); + expr_ref eps(re().mk_to_re(u().str.mk_empty(seq_sort)), m); + + // Apply predicate to the element + array_util autil(m); + expr* args[2] = { pred, m_ele }; + expr_ref cond(autil.mk_select(2, args), m); + return mk_ite(cond, eps, empty); + } + + // Extract head character and remaining tail from a sequence + // s1 is the first part, s2 is the continuation (from str.concat(s1, s2)) + bool derive::get_head_tail(expr* s1, expr* s2, expr_ref& hd, expr_ref& tl) { + expr* ch = nullptr; + expr* a = nullptr, * b = nullptr; + if (u().str.is_unit(s1, ch)) { + hd = ch; + tl = s2; + return true; + } + if (u().str.is_concat(s1, a, b)) { + expr_ref new_s2(u().str.mk_concat(b, s2), m); + return get_head_tail(a, new_s2, hd, tl); + } + zstring zs; + if (u().str.is_string(s1, zs) && zs.length() > 0) { + hd = m_util.mk_char(zs[0]); + if (zs.length() == 1) + tl = s2; + else { + expr_ref rest(u().str.mk_string(zs.extract(1, zs.length() - 1)), m); + tl = u().str.mk_concat(rest, s2); + } + return true; + } + return false; + } + + // ------------------------------------------------------- + // Normalize reverse + // ------------------------------------------------------- + + expr_ref derive::mk_regex_reverse(expr* r) { + expr* r1 = nullptr, * r2 = nullptr, * c = nullptr; + unsigned lo = 0, hi = 0; + expr_ref result(m); + if (re().is_empty(r) || re().is_range(r) || re().is_epsilon(r) || re().is_full_seq(r) || + re().is_full_char(r) || re().is_dot_plus(r) || re().is_of_pred(r)) + result = r; + else if (re().is_to_re(r)) + result = re().mk_reverse(r); + else if (re().is_reverse(r, r1)) + result = r1; + else if (re().is_concat(r, r1, r2)) + result = re().mk_concat(mk_regex_reverse(r2), mk_regex_reverse(r1)); + else if (m.is_ite(r, c, r1, r2)) + result = m.mk_ite(c, mk_regex_reverse(r1), mk_regex_reverse(r2)); + else if (re().is_union(r, r1, r2)) { + auto a1 = mk_regex_reverse(r1); + auto b1 = mk_regex_reverse(r2); + result = re().mk_union(a1, b1); + } + else if (re().is_intersection(r, r1, r2)) { + auto a1 = mk_regex_reverse(r1); + auto b1 = mk_regex_reverse(r2); + result = re().mk_inter(a1, b1); + } + else if (re().is_diff(r, r1, r2)) { + auto a1 = mk_regex_reverse(r1); + auto b1 = mk_regex_reverse(r2); + result = re().mk_diff(a1, b1); + } + else if (re().is_star(r, r1)) + result = re().mk_star(mk_regex_reverse(r1)); + else if (re().is_plus(r, r1)) + result = re().mk_plus(mk_regex_reverse(r1)); + else if (re().is_loop(r, r1, lo)) + result = re().mk_loop(mk_regex_reverse(r1), lo); + else if (re().is_loop(r, r1, lo, hi)) + result = re().mk_loop_proper(mk_regex_reverse(r1), lo, hi); + else if (re().is_opt(r, r1)) + result = re().mk_opt(mk_regex_reverse(r1)); + else if (re().is_complement(r, r1)) + result = re().mk_complement(mk_regex_reverse(r1)); + else + result = re().mk_reverse(r); + return result; + } + + // ------------------------------------------------------- + // Nullability - uses info class from seq_decl_plugin.h + // ------------------------------------------------------- + + expr_ref derive::is_nullable(expr* r) { + SASSERT(m_util.is_re(r) || m_util.is_seq(r)); + expr* r1 = nullptr, * r2 = nullptr, * cond = nullptr; + sort* seq_sort = nullptr; + unsigned lo = 0, hi = 0; + zstring s1; + if (m_util.is_re(r)) { + auto info = re().get_info(r); + switch (info.nullable) { + case l_true: return expr_ref(m.mk_true(), m); + case l_false: return expr_ref(m.mk_false(), m); + default: break; + } + } + expr_ref result(m); + if (re().is_concat(r, r1, r2) || + re().is_intersection(r, r1, r2)) { + m_br.mk_and(is_nullable(r1), is_nullable(r2), result); + } + else if (re().is_union(r, r1, r2)) { + m_br.mk_or(is_nullable(r1), is_nullable(r2), result); + } + else if (re().is_diff(r, r1, r2)) { + m_br.mk_not(is_nullable(r2), result); + m_br.mk_and(result, is_nullable(r1), result); + } + else if (re().is_xor(r, r1, r2)) { + m_br.mk_xor(is_nullable(r1), is_nullable(r2), result); + } + else if (re().is_star(r) || + re().is_opt(r) || + re().is_full_seq(r) || + re().is_epsilon(r) || + (re().is_loop(r, r1, lo) && lo == 0) || + (re().is_loop(r, r1, lo, hi) && lo == 0)) { + result = m.mk_true(); + } + else if (re().is_full_char(r) || + re().is_empty(r) || + re().is_of_pred(r) || + re().is_range(r)) { + result = m.mk_false(); + } + else if (re().is_plus(r, r1) || + (re().is_loop(r, r1, lo) && lo > 0) || + (re().is_loop(r, r1, lo, hi) && lo > 0) || + (re().is_reverse(r, r1))) { + result = is_nullable(r1); + } + else if (re().is_complement(r, r1)) { + m_br.mk_not(is_nullable(r1), result); + } + else if (re().is_to_re(r, r1)) { + result = is_nullable(r1); + } + else if (m.is_ite(r, cond, r1, r2)) { + m_br.mk_ite(cond, is_nullable(r1), is_nullable(r2), result); + } + else if (m_util.is_re(r, seq_sort)) { + result = is_nullable_symbolic_regex(r, seq_sort); + } + else if (u().str.is_concat(r, r1, r2)) { + m_br.mk_and(is_nullable(r1), is_nullable(r2), result); + } + else if (u().str.is_empty(r)) { + result = m.mk_true(); + } + else if (u().str.is_unit(r)) { + result = m.mk_false(); + } + else if (u().str.is_string(r, s1)) { + result = m.mk_bool_val(s1.length() == 0); + } + else { + SASSERT(m_util.is_seq(r)); + result = m.mk_eq(u().str.mk_empty(r->get_sort()), r); + } + return result; + } + + expr_ref derive::is_nullable_symbolic_regex(expr* r, sort* seq_sort) { + SASSERT(m_util.is_re(r)); + expr* elem = nullptr, * r1 = r, * r2 = nullptr, * s = nullptr; + expr_ref elems(u().str.mk_empty(seq_sort), m); + expr_ref result(m); + while (re().is_derivative(r1, elem, r2)) { + if (u().str.is_empty(elems)) + elems = u().str.mk_unit(elem); + else + elems = u().str.mk_concat(u().str.mk_unit(elem), elems); + r1 = r2; + } + if (re().is_to_re(r1, s)) { + result = m.mk_eq(elems, s); + return result; + } + result = re().mk_in_re(u().str.mk_empty(seq_sort), r); + return result; + } + + // ------------------------------------------------------- + // Smart constructors with simplification + // ------------------------------------------------------- + + + // Extract character range [lo, hi] from a derivative condition. + // Conditions are of the form: + // ele == c → range [c, c] + // char_le(lo_expr, ele) && char_le(ele, hi_expr) → range [lo, hi] + // char_le(lo_expr, ele) → range [lo, max_char] + // char_le(ele, hi_expr) → range [0, hi] + // Returns false if not a recognizable range condition. + // Predicate implication for character range conditions. + // Returns true if: whenever cond_a is true, cond_b must also be true. + // pred_implies(sign_a, a, sign_b, b): does (sign_a ? ¬a : a) imply (sign_b ? ¬b : b)? + bool derive::pred_implies(bool sign_a, expr* a, bool sign_b, expr* b) { + // Same atom: check sign compatibility + if (a == b) return sign_a == sign_b; + + // Both negated: ¬a → ¬b iff b → a, i.e. pred_implies(false, b, false, a) + if (sign_a && sign_b) + return pred_implies(false, b, false, a); + + unsigned lo_a, hi_a, lo_b, hi_b; + bool neg_a, neg_b; + + if (!sign_a && !sign_b) { + // a → b: range_a ⊆ range_b + if (u().is_char_const_range(m_ele, a, lo_a, hi_a, neg_a) && !neg_a && + u().is_char_const_range(m_ele, b, lo_b, hi_b, neg_b) && !neg_b) + return lo_b <= lo_a && hi_a <= hi_b; + } + else if (!sign_a && sign_b) { + // a → ¬b: range_a ∩ range_b = ∅ + if (u().is_char_const_range(m_ele, a, lo_a, hi_a, neg_a) && !neg_a && + u().is_char_const_range(m_ele, b, lo_b, hi_b, neg_b) && !neg_b) + return hi_a < lo_b || hi_b < lo_a; + } + else if (sign_a && !sign_b) { + // ¬a → b: complement of range_a ⊆ range_b + if (u().is_char_const_range(m_ele, a, lo_a, hi_a, neg_a) && !neg_a && + u().is_char_const_range(m_ele, b, lo_b, hi_b, neg_b) && !neg_b) + return lo_b == 0 && hi_b >= u().max_char(); + } + + return false; + } + + bool derive::pred_implies(expr* a, expr* b) { + bool sign_a = m.is_not(a, a); + bool sign_b = m.is_not(b, b); + return pred_implies(sign_a, a, sign_b, b); + } + + expr_ref derive::mk_xor(expr *a, expr *b) { + return mk_core(OP_RE_XOR, a, b); + } + + expr_ref derive::mk_xor_core(expr *a, expr *b) { + + return m_re.mk_xor0(a, b); + } + + expr_ref derive::mk_core(decl_kind k, expr* a, expr* b) { + expr *pe = get_path_expr(); + expr *cached = nullptr; + auto& cache = k == OP_RE_UNION ? union_cache() : k == OP_RE_INTERSECT ? inter_cache() : xor_cache(); + if (cache.find(a, b, pe, cached)) + return expr_ref(cached, m); + expr_ref result(m); + // ITE handling with path pruning + auto inter_op = [&](expr *x, expr *y) { return mk_inter(x, y); }; + auto union_op = [&](expr *x, expr *y) { return mk_union(x, y); }; + auto xor_op = [&](expr *x, expr *y) { return mk_xor(x, y); }; + switch (k) { + case OP_RE_UNION: + if (m_derivative_kind == derivative_kind::brzozowski_t) + result = hoist_ite(a, b, union_op); + if (!result) + result = mk_union_core(a, b); + break; + case OP_RE_INTERSECT: + result = hoist_ite(a, b, inter_op); + if (!result) + result = mk_inter_core(a, b); + break; + case OP_RE_XOR: + result = hoist_ite(a, b, xor_op); + if (!result) + result = mk_xor_core(a, b); + break; + default: + UNREACHABLE(); + break; + } + // Store in cache + cache.insert(a, b, pe, result); + m_trail.push_back(a); + m_trail.push_back(b); + m_trail.push_back(pe); + m_trail.push_back(result); + return result; + } + + expr_ref derive::mk_union(expr* a, expr* b) { + return mk_core(OP_RE_UNION, a, b); + } + + // Lightweight structural subsumption: checks if L(a) ⊆ L(b) + bool derive::is_subset(expr* a, expr* b) { + return m_re.is_subset(a, b); + } + + bool derive::are_complements(expr* a, expr* b) { + expr* c = nullptr; + if (re().is_complement(a, c) && c == b) return true; + if (re().is_complement(b, c) && c == a) return true; + return false; + } + + expr_ref derive::mk_union_core(expr* a, expr* b) { + + // Identity: none ∪ R = R (none is the unit of union) + // Idempotence: R ∪ R = R + // Absorption: Σ* ∪ R = Σ* + // Without these the derivative of an intersection accumulates + // un-simplified unions such as union(inter, union(none, none)), + // producing many syntactically distinct but semantically equal + // states. That defeats state dedup in the emptiness/bisim closure + // and makes contains-pattern intersections blow up. + if (re().is_empty(a)) return expr_ref(b, m); + if (re().is_empty(b)) return expr_ref(a, m); + if (a == b) return expr_ref(a, m); + if (re().is_full_seq(a) || re().is_full_seq(b)) + return expr_ref(re().mk_full_seq(a->get_sort()), m); + + // Flatten the disjuncts of `a` and `b` into a single reduced set, + // applying subsumption, prefix factoring and same-condition ITE merge + // against *every* existing member (see add_union_elem). Pairwise + // reduction on the two direct operands alone misses a term subsumed by a + // member nested inside an existing union — the root cause of the + // loop ∩ comp derivative accumulating one a{0,k}·R state per k. + // + // The flattening uses an explicit worklist rather than recursion: a + // recursive insert-into-union recurses with depth proportional to the + // union width and overflows the stack on wide range-product unions. + expr_ref_vector set(m); + ptr_vector todo; + todo.push_back(b); + todo.push_back(a); + while (!todo.empty()) { + expr* e = todo.back(); + todo.pop_back(); + expr *e1, *e2; + if (re().is_union(e, e1, e2)) { + todo.push_back(e2); + todo.push_back(e1); + continue; + } + if (re().is_empty(e)) + continue; + if (re().is_full_seq(e)) + return expr_ref(re().mk_full_seq(a->get_sort()), m); + add_union_elem(set, e); + } + + if (set.empty()) + return expr_ref(re().mk_empty(a->get_sort()), m); + expr_ref r(set.get(0), m); + for (unsigned i = 1; i < set.size(); ++i) + r = expr_ref(re().mk_union(r, set.get(i)), m); + return r; + } + + // Reduce `e` against the disjunct set `set` and insert it, maintaining the + // invariant that no member subsumes another. Iterative (bounded loop) to + // avoid the stack overflow a recursive formulation incurs on wide unions. + void derive::add_union_elem(expr_ref_vector& set, expr* e0) { + expr_ref e(e0, m); + bool changed = true; + while (changed) { + changed = false; + for (unsigned i = 0; i < set.size(); ++i) { + expr* s = set.get(i); + if (s == e) + return; // duplicate + // Subsumption: L(e) ⊆ L(s) ⇒ drop e; L(s) ⊆ L(e) ⇒ drop s. + if (is_subset(e, s)) + return; + if (is_subset(s, e)) { + set.set(i, set.back()); + set.pop_back(); + changed = true; + break; + } + // Same-condition ITE merge: + // ite(c,t1,e1) ∪ ite(c,t2,e2) → ite(c, t1∪t2, e1∪e2). + // Brings the bodies of same-condition ITE alternatives (e.g. + // a{0,k-1}·R and a{0,k}·R) into a common union where the subset + // rule above collapses them, preventing O(N) state blowup. + expr *c1, *t1, *el1, *c2, *t2, *el2; + if (m.is_ite(e, c1, t1, el1) && m.is_ite(s, c2, t2, el2) && c1 == c2) { + set.set(i, set.back()); + set.pop_back(); + e = mk_ite(c1, mk_union(t1, t2), mk_union(el1, el2)); + changed = true; + break; + } + // NB: a `p·x ∪ p·y → p·(x ∪ y)` prefix-factoring rule used to + // live here. It is semantically valid but harmful: factoring a + // common nullable-star prefix (e.g. (a|b)*) produces nested + // `S·(… ∪ …)` leaves that never stabilise to a bounded state + // family, so the bisimulation/emptiness closure keys ever-larger + // distinct expressions and fails to dedup. Leaving the union in + // distributed form keeps each disjunct a "position" state, which + // matches the classical Brzozowski derivative and lets bisim + // close in a bounded number of steps on flat-vs-loop equivalence. + } + } + set.push_back(e); + } + + expr_ref derive::mk_inter(expr* a, expr* b) { + return mk_core(OP_RE_INTERSECT, a, b); + } + + expr_ref derive::mk_inter_core(expr* a, expr* b) { + + // Subsumption covers: a==b, empty(a), empty(b), full(a), full(b), etc. + if (is_subset(a, b)) return expr_ref(a, m); + if (is_subset(b, a)) return expr_ref(b, m); + + // Complement absorption: r ∩ ~r = ∅ + expr *c = nullptr, *d = nullptr; + if (re().is_complement(a, c) && c == b) + return expr_ref(re().mk_empty(a->get_sort()), m); + if (re().is_complement(b, c) && c == a) + return expr_ref(re().mk_empty(a->get_sort()), m); + if (re().is_complement(a, c) && re().is_complement(b, d)) + return expr_ref(re().mk_complement(mk_union_core(c, d)), m); + + + + // Distribution of intersection over union: (x ∪ y) ∩ b → (x ∩ b) ∪ (y ∩ b). + // + // This is done only in *antimirov* mode. Antimirov derivatives expose + // nondeterminism by lifting unions to the top, so the emptiness/membership + // solver (get_derivative_targets / mk_deriv_accept) can decompose the + // transition regex into a set of individual ground product states + // inter(A_i, B_j) and check each separately — detecting emptiness fast. + // + // In *brzozowski* mode (used by the regex_bisim equivalence procedure) + // we deliberately keep the intersection *above* the union, mirroring the + // classical product DFA. Distributing there would lift unions above + // intersections and turn one inter-state into a union of inter-states at + // every derivative step, doubling the number of distinct bisimulation + // states each step (super-linear blowup on product/equiv encodings such + // as (R1 ∩ ~R2) = (~R1 ∩ R2)). The cofactor enumeration handles an + // intersection sitting above a union fine: get_cofactors uses + // decompose_ite, which hoists the var-0 conditions out of arbitrarily + // nested inter/union leaves, so states stay ground either way. + expr *u1 = nullptr, *u2 = nullptr; + if (m_derivative_kind == derivative_kind::antimirov_t) { + if (re().is_union(a, u1, u2)) + return mk_union(mk_inter(u1, b), mk_inter(u2, b)); + if (re().is_union(b, u1, u2)) + return mk_union(mk_inter(a, u1), mk_inter(a, u2)); + } + + // Base case: build raw intersection + return m_re.mk_inter(a, b); + } + + + expr_ref derive::mk_concat(expr* a, expr* b) { + sort* seq_s = nullptr, * ele_s = nullptr; + VERIFY(m_util.is_re(a, seq_s)); + VERIFY(u().is_seq(seq_s, ele_s)); + if (re().is_empty(a)) return expr_ref(a, m); + if (re().is_empty(b)) return expr_ref(b, m); + if (re().is_epsilon(a)) return expr_ref(b, m); + if (re().is_epsilon(b)) return expr_ref(a, m); + if (re().is_full_seq(a) && re().is_full_seq(b)) + return expr_ref(a, m); + if (re().is_full_char(a) && re().is_full_seq(b)) + return expr_ref(re().mk_plus(re().mk_full_char(a->get_sort())), m); + if (re().is_full_seq(a) && re().is_full_char(b)) + return expr_ref(re().mk_plus(re().mk_full_char(a->get_sort())), m); + + // to_re(s1) · to_re(s2) → to_re(s1 ++ s2) + expr* s1 = nullptr, * s2 = nullptr; + if (re().is_to_re(a, s1) && re().is_to_re(b, s2)) + return expr_ref(re().mk_to_re(u().str.mk_concat(s1, s2)), m); + + // r* · r* → r* + + expr* a1 = nullptr, *a2 = nullptr, * b1 = nullptr; + + if (re().is_star(a, a1) && re().is_star(b, b1) && a1 == b1) + return expr_ref(a, m); + + // Right-associate: (a · b) · c → a · (b · c) + + if (re().is_concat(a, a1, a2)) + return mk_concat(a1, mk_concat(a2, b)); + + return expr_ref(re().mk_concat(a, b), m); + } + + expr_ref derive::mk_complement(expr* a) { + // Check path-aware op cache + expr* pe = get_path_expr(); + expr* cached = nullptr; + if (complement_cache().find(a, pe, cached)) + return expr_ref(cached, m); + + expr_ref result = mk_complement_core(a); + + // Store in cache + complement_cache().insert(a, pe, result); + m_trail.push_back(a); + m_trail.push_back(pe); + m_trail.push_back(result); + return result; + } + + expr_ref derive::mk_complement_core(expr* a) { + // ~~r → r + expr* r = nullptr; + if (re().is_complement(a, r)) + return expr_ref(r, m); + // ~∅ → Σ* + if (re().is_empty(a)) + return expr_ref(re().mk_full_seq(a->get_sort()), m); + // ~Σ* → ∅ + if (re().is_full_seq(a)) + return expr_ref(re().mk_empty(a->get_sort()), m); + + // Push through ITE with path pruning: ~(ite(c, t, e)) → ite(c, ~t, ~e) + expr* c, * t, * e; + if (m.is_ite(a, c, t, e)) { + auto comp_op = [&](expr* x) { return mk_complement(x); }; + expr_ref r = apply_ite(c, t, e, comp_op); + if (r) return r; + return expr_ref(re().mk_full_seq(a->get_sort()), m); + } + + // ~ε → .+ + sort* s = nullptr; + expr* r1 = nullptr; + if (re().is_to_re(a, r1) && u().str.is_empty(r1)) { + VERIFY(m_util.is_re(a, s)); + return expr_ref(re().mk_plus(re().mk_full_char(a->get_sort())), m); + } + + // De Morgan: push complement through union/intersection to the leaves + // so that complemented subterms stay invariant across successive + // derivatives and unions are exposed at the top. This keeps the + // symbolic derivative in transition-regex normal form and lets the + // antimirov non-emptiness check decide each union alternative + // separately. In particular ~(Σ*a ∪ ε) → ~(Σ*a) ∩ .+, so the ~(Σ*a) + // state is shared rather than growing into ~(Σ*a ∪ ε ∪ ...), which + // otherwise defeats dead-state detection on loop ∩ comp regexes. + expr* e1 = nullptr, *e2 = nullptr; + if (re().is_union(a, e1, e2)) + return mk_inter(mk_complement(e1), mk_complement(e2)); + if (re().is_intersection(a, e1, e2)) + return mk_union(mk_complement(e1), mk_complement(e2)); + + return expr_ref(re().mk_complement(a), m); + } + + expr_ref derive::mk_ite(expr* c, expr* t, expr* e) { + if (m.is_true(c) || t == e) + return expr_ref(t, m); + if (m.is_false(c)) + return expr_ref(e, m); + // Use path-aware condition evaluation + lbool cond_val = eval_path_cond(c); + if (cond_val == l_true) return expr_ref(t, m); + if (cond_val == l_false) return expr_ref(e, m); + return expr_ref(m.mk_ite(c, t, e), m); + } + + // ------------------------------------------------------- + // Distribute concat through ITE/union structure of derivative + // ------------------------------------------------------- + + expr_ref derive::mk_deriv_concat(expr* d, expr* tail) { + // Check op cache + expr* cached = nullptr; + if (concat_cache().find(d, tail, cached)) + return expr_ref(cached, m); + + expr_ref result = mk_deriv_concat_core(d, tail); + + // Store in cache + concat_cache().insert(d, tail, result); + m_trail.push_back(d); + m_trail.push_back(tail); + m_trail.push_back(result); + return result; + } + + expr_ref derive::mk_deriv_concat_core(expr* d, expr* tail) { + expr_ref _d(d, m), _tail(tail, m); + expr* c, * t, * e; + + if (re().is_empty(d)) + return expr_ref(d, m); + if (re().is_epsilon(d)) + return expr_ref(tail, m); + + if (m.is_ite(d, c, t, e)) { + expr_ref then_r = mk_deriv_concat(t, tail); + expr_ref else_r = mk_deriv_concat(e, tail); + return mk_ite(c, then_r, else_r); + } + + // (t ∪ e) · tail → (t · tail) ∪ (e · tail) + // + // Right-distribution of concatenation over a union derivative head. Done + // only in *antimirov* mode (the membership/accept path): exposing the union + // at the top splits one residual into separate ground product states, which + // lets the solver short-circuit witness search and keeps counting patterns + // such as (.*a.{n}b.*) linear (NFA-style) instead of 2^n (DFA-style). + // In *brzozowski* mode (bisim equivalence and the brzozowski emptiness + // enumeration) we keep the union below the concatenation so transition + // regexes stay deterministic, mirroring the classical product DFA. + if (m_derivative_kind == derivative_kind::antimirov_t && re().is_union(d, t, e)) { + expr_ref left = mk_deriv_concat(t, tail); + expr_ref right = mk_deriv_concat(e, tail); + return mk_union(left, right); + } + + return mk_concat(d, tail); + } + + // ------------------------------------------------------- + // Path management for inline pruning + // ------------------------------------------------------- + + lbool derive::push(expr* c, bool sign) { + // Check if (c, sign) is already determined by the path + lbool cv = eval_path_cond(c); + if (cv == l_true && !sign) return l_true; // c implied true, push(c,false) is redundant + if (cv == l_false && sign) return l_true; // c implied false, push(c,true) is redundant + if (cv == l_true && sign) return l_false; // c implied true, push(c,true) contradicts + if (cv == l_false && !sign) return l_false; // c implied false, push(c,false) contradicts + + // Save current state + unsigned saved_path_sz = m_path.size(); + unsigned saved_intervals_sz = m_intervals.size(); + unsigned saved_intervals_start = m_intervals_start; + expr* saved_path_expr = m_path_expr; + + // Push atoms onto path and check for contradiction or implication + lbool result = push_path_atoms(c, sign); + if (result != l_undef) { + m_path.shrink(saved_path_sz); + m_intervals.shrink(saved_intervals_sz); + m_intervals_start = saved_intervals_start; + return result; + } + + // Update intervals + result = push_intervals_impl(c, sign); + if (result != l_undef) { + m_path.shrink(saved_path_sz); + m_intervals.shrink(saved_intervals_sz); + m_intervals_start = saved_intervals_start; + return result; + } + + // Update path expression + expr* atom = sign ? m.mk_not(c) : c; + m_path_expr = m.mk_and(m_path_expr, atom); + m_trail.push_back(m_path_expr); + + // Commit: save state for pop() + m_path_stack.push_back({ saved_path_sz, saved_intervals_sz, saved_intervals_start, saved_path_expr }); + return l_undef; + } + + void derive::pop() { + SASSERT(!m_path_stack.empty()); + auto const& saved = m_path_stack.back(); + m_path.shrink(saved.path_sz); + m_intervals.shrink(saved.intervals_sz); + m_intervals_start = saved.intervals_start; + m_path_expr = saved.path_expr; + m_path_stack.pop_back(); + } + + // Binary apply_ite: hoist ite(c, t, e) op r with path pruning + expr_ref derive::apply_ite(expr* c, expr* t, expr* e, expr* r, std::function apply_op) { + expr_ref then_br(m), else_br(m); + switch (push(c, false)) { + case l_true: return apply_op(t, r); + case l_undef: then_br = apply_op(t, r); pop(); break; + case l_false: break; + } + switch (push(c, true)) { + case l_true: return apply_op(e, r); + case l_undef: else_br = apply_op(e, r); pop(); break; + case l_false: break; + } + if (then_br && else_br) return mk_ite(c, then_br, else_br); + if (then_br) return then_br; + if (else_br) return else_br; + return expr_ref(nullptr, m); + } + + // Same-condition merge: ite(c, t1, e1) op ite(c, t2, e2) → ite(c, t1 op t2, e1 op e2) + expr_ref derive::apply_ite(expr* c, expr* t1, expr* e1, expr* t2, expr* e2, std::function apply_op) { + expr_ref then_br(m), else_br(m); + switch (push(c, false)) { + case l_true: return apply_op(t1, t2); + case l_undef: then_br = apply_op(t1, t2); pop(); break; + case l_false: break; + } + switch (push(c, true)) { + case l_true: return apply_op(e1, e2); + case l_undef: else_br = apply_op(e1, e2); pop(); break; + case l_false: break; + } + if (then_br && else_br) return mk_ite(c, then_br, else_br); + if (then_br) return then_br; + if (else_br) return else_br; + return expr_ref(nullptr, m); + } + + // Unary apply_ite: hoist ite(c, t, e) through unary op with path pruning + expr_ref derive::apply_ite(expr* c, expr* t, expr* e, std::function apply_op) { + expr_ref then_br(m), else_br(m); + switch (push(c, false)) { + case l_true: return apply_op(t); + case l_undef: then_br = apply_op(t); pop(); break; + case l_false: break; + } + switch (push(c, true)) { + case l_true: return apply_op(e); + case l_undef: else_br = apply_op(e); pop(); break; + case l_false: break; + } + if (then_br && else_br) return mk_ite(c, then_br, else_br); + if (then_br) return then_br; + if (else_br) return else_br; + return expr_ref(nullptr, m); + } + + // Common ITE dispatch for binary ops (union/inter). + // Returns nullptr if neither a nor b is ITE. + expr_ref derive::hoist_ite(expr* a, expr* b, std::function apply_op) { + expr *c1, *t1, *e1, *c2, *t2, *e2; + if (m.is_ite(a, c1, t1, e1) && m.is_ite(b, c2, t2, e2) && c1->get_id() > c2->get_id()) + std::swap(a, b); + if (m.is_ite(a, c1, t1, e1) && m.is_ite(b, c2, t2, e2)) { + expr_ref r(m); + if (c1 == c2) + r = apply_ite(c1, t1, e1, t2, e2, apply_op); + else + r = apply_ite(c1, t1, e1, b, apply_op); + if (r) return r; + return expr_ref(re().mk_empty(a->get_sort()), m); + } + // Single-ITE hoisting: must always recurse to maintain path-aware + // soundness — falling back to a non-path-aware structural rewrite + // here would bake unreachable branches into the result tree. + if (m.is_ite(a, c1, t1, e1)) { + expr_ref r = apply_ite(c1, t1, e1, b, apply_op); + if (r) return r; + return expr_ref(re().mk_empty(a->get_sort()), m); + } + if (m.is_ite(b, c2, t2, e2)) { + expr_ref r = apply_ite(c2, t2, e2, a, apply_op); + if (r) return r; + return expr_ref(re().mk_empty(a->get_sort()), m); + } + return expr_ref(nullptr, m); + } + + // Push signed atoms onto m_path. Returns l_true if implied, l_false if contradicted, l_undef if pushed. + lbool derive::push_path_atoms(expr* c, bool sign) { + // Check if (c, sign) is already determined by the path + for (auto const& [cond, csign] : m_path) { + if (c == cond) + return csign == sign ? l_true : l_false; + expr* lhs1 = nullptr, * rhs1 = nullptr, * lhs2 = nullptr, * rhs2 = nullptr; + // x = v, v != w |-> x != w + if (!csign && m.is_eq(cond, lhs1, rhs1) && m.is_eq(c, lhs2, rhs2)) { + if (m.is_value(lhs1)) std::swap(lhs1, rhs1); + if (m.is_value(lhs2)) std::swap(lhs2, rhs2); + if (lhs1 == lhs2 && m.are_distinct(rhs1, rhs2)) + return sign ? l_true : l_false; + } + } + + // Composite: conjunction assumed true, or disjunction assumed false + if ((!sign && m.is_and(c)) || (sign && m.is_or(c))) { + bool all_implied = true; + for (expr* arg : *to_app(c)) { + lbool r = push_path_atoms(arg, sign); + if (r == l_false) return l_false; + if (r == l_undef) all_implied = false; + } + return all_implied ? l_true : l_undef; + } + + // Atomic: push onto path + m_path.push_back({ c, sign }); + return l_undef; + } + + // Update m_intervals based on the condition. Returns l_true if implied, l_false if inconsistent, l_undef if pushed. + // Operates on the active suffix m_intervals[m_intervals_start..end]. + // On modification, appends new intervals and updates m_intervals_start. + lbool derive::push_intervals_impl(expr* c, bool sign) { + unsigned lo = 0, hi = 0; + bool negated = false; + if (m_util.is_char_const_range(m_ele, c, lo, hi, negated)) { + bool effective_neg = (negated != sign); + if (!effective_neg) { + if (lo <= hi) { + // Check if current intervals already imply [lo,hi] + bool already_subset = true; + for (unsigned i = m_intervals_start; i < m_intervals.size(); ++i) { + if (m_intervals[i].first < lo || m_intervals[i].second > hi) { already_subset = false; break; } + } + if (already_subset) return l_true; + intersect_intervals(lo, hi); + } else { + // lo > hi means empty range — contradiction + return l_false; + } + } else { + if (lo <= hi) { + // Check if current intervals already exclude [lo,hi] + bool already_excluded = true; + for (unsigned i = m_intervals_start; i < m_intervals.size(); ++i) { + if (m_intervals[i].first <= hi && m_intervals[i].second >= lo) { already_excluded = false; break; } + } + if (already_excluded) return l_true; + exclude_interval(lo, hi); + } + } + } else if ((!sign && m.is_and(c)) || (sign && m.is_or(c))) { + bool all_implied = true; + for (expr* arg : *to_app(c)) { + lbool r = push_intervals_impl(arg, sign); + if (r == l_false) return l_false; + if (r == l_undef) all_implied = false; + } + unsigned n = m_intervals.size() - m_intervals_start; + return all_implied ? l_true : (n == 0 ? l_false : l_undef); + } + unsigned n = m_intervals.size() - m_intervals_start; + return n == 0 ? l_false : l_undef; + } + + // Evaluate a condition against the current path and intervals. + lbool derive::eval_path_cond(expr* c) { + // First try static evaluation (concrete m_ele, tautologies) + lbool v = eval_cond(c); + if (v != l_undef) return v; + + // Check against path atoms + for (auto const& [cond, sign] : m_path) { + if (c == cond) + return sign ? l_false : l_true; + } + + // Check against intervals + v = eval_range_cond(c); + if (v != l_undef) return v; + + // Check pred_implies from path atoms + for (auto const& [cond, sign] : m_path) { + if (pred_implies(sign, cond, false, c)) + return l_true; + if (pred_implies(sign, cond, true, c)) + return l_false; + } + + return l_undef; + } + + // ------------------------------------------------------- + // Condition evaluation helpers + // ------------------------------------------------------- + + lbool derive::eval_cond(expr* cond) { + expr* e1 = nullptr; + + if (m.is_true(cond)) return l_true; + if (m.is_false(cond)) return l_false; + + // Use is_char_const_range to evaluate conditions involving m_ele + unsigned lo = 0, hi = 0, ele_val = 0; + bool negated = false; + if (m_util.is_char_const_range(m_ele, cond, lo, hi, negated) && u().is_const_char(m_ele, ele_val)) { + bool in_range = (lo <= ele_val && ele_val <= hi); + return (in_range != negated) ? l_true : l_false; + } + + // Handle self-equality and constant comparisons not involving m_ele + expr* lhs = nullptr, * rhs = nullptr; + if (m.is_eq(cond, lhs, rhs) && lhs == rhs) + return l_true; + + unsigned vl = 0, vr = 0; + if (u().is_char_le(cond, lhs, rhs)) { + if (u().is_const_char(lhs, vl) && u().is_const_char(rhs, vr)) + return vl <= vr ? l_true : l_false; + if (u().is_const_char(lhs, vl) && vl == 0) + return l_true; + if (u().is_const_char(rhs, vr) && vr == u().max_char()) + return l_true; + } + + // not(e1) + if (m.is_not(cond, e1)) + return ~eval_cond(e1); + + // and(...) + if (m.is_and(cond)) { + lbool r = l_true; + for (expr* arg : *to_app(cond)) { + lbool v = eval_cond(arg); + if (v == l_false) return l_false; + if (v == l_undef) r = l_undef; + } + return r; + } + + // or(...) + if (m.is_or(cond)) { + lbool r = l_false; + for (expr* arg : *to_app(cond)) { + lbool v = eval_cond(arg); + if (v == l_true) return l_true; + if (v == l_undef) r = l_undef; + } + return r; + } + + return l_undef; + } + + lbool derive::eval_range_cond(expr* c) { + unsigned n = m_intervals.size() - m_intervals_start; + if (n == 0) + return l_false; + unsigned lo = 0, hi = 0; + bool negated = false; + if (!m_util.is_char_const_range(m_ele, c, lo, hi, negated)) + return l_undef; + if (lo > hi) { + return negated ? l_true : l_false; + } + // Check if [lo, hi] overlaps with intervals and/or contains all intervals + bool any_overlap = false; + bool all_contained = true; + for (unsigned i = m_intervals_start; i < m_intervals.size(); ++i) { + auto [r_lo, r_hi] = m_intervals[i]; + if (std::max(r_lo, lo) <= std::min(r_hi, hi)) + any_overlap = true; + if (r_lo < lo || r_hi > hi) + all_contained = false; + } + if (!negated) { + if (!any_overlap) return l_false; + if (all_contained) return l_true; + } else { + if (all_contained) return l_false; + if (!any_overlap) return l_true; + } + return l_undef; + } + + // Intersect the active suffix m_intervals[m_intervals_start..end] with [lo, hi] + void derive::intersect_intervals(unsigned lo, unsigned hi) { + // Copy active suffix to end, update start, then filter + unsigned old_sz = m_intervals.size(); + for (unsigned i = m_intervals_start; i < old_sz; ++i) { + auto e = m_intervals[i]; + m_intervals.push_back(e); + } + m_intervals_start = old_sz; + // Filter in-place within new suffix: drop intervals disjoint from [lo,hi], + // keep the intersection for overlapping ones. + unsigned j = m_intervals_start; + for (unsigned i = m_intervals_start; i < m_intervals.size(); ++i) { + auto [lo1, hi1] = m_intervals[i]; + if (hi < lo1 || lo > hi1) + continue; // disjoint with this interval — drop it + m_intervals[j++] = {std::max(lo1, lo), std::min(hi1, hi)}; + } + m_intervals.shrink(j); + } + + // Exclude [lo, hi] from the active suffix m_intervals[m_intervals_start..end] + void derive::exclude_interval(unsigned lo, unsigned hi) { + unsigned max_char = u().max_char(); + if (lo == 0 && hi >= max_char) { m_intervals_start = m_intervals.size(); return; } + if (lo == 0) { intersect_intervals(hi + 1, max_char); return; } + if (hi >= max_char) { intersect_intervals(0, lo - 1); return; } + // Each interval [ilo, ihi] minus [lo, hi] → up to 2 pieces + // Append new results past the end, then move start + unsigned old_start = m_intervals_start; + unsigned old_sz = m_intervals.size(); + for (unsigned i = old_start; i < old_sz; ++i) { + auto [ilo, ihi] = m_intervals[i]; + if (ihi < lo || ilo > hi) { + auto e = m_intervals[i]; + m_intervals.push_back(e); + } else { + if (ilo < lo) + m_intervals.push_back({ilo, lo - 1}); + if (ihi > hi) + m_intervals.push_back({hi + 1, ihi}); + } + } + m_intervals_start = old_sz; + } + + // ------------------------------------------------------- + // Cofactor enumeration over a transition regex + // ------------------------------------------------------- + + expr_ref derive::clean_leaf(expr* r) { + expr* a = nullptr, * b = nullptr; + if (re().is_union(r, a, b)) + return mk_union(clean_leaf(a), clean_leaf(b)); + if (re().is_intersection(r, a, b)) + return mk_inter(clean_leaf(a), clean_leaf(b)); + return expr_ref(r, m); + } + + void derive::get_cofactors_rec(expr* r, expr_ref_pair_vector& result) { + // Hoist the (first) if-then-else condition to the top of r, splitting it + // into the equivalent ite(c, th, el); when r contains no ite it is a + // leaf of the transition regex. + expr_ref c(m), th(m), el(m); + if (!m_br.decompose_ite(r, c, th, el)) { + // Re-normalize the leaf: decompose_ite substitutes ITE branches + // structurally so the leaf may carry un-simplified union(_, none) + // / inter(_, none) nodes. Cleaning them keeps semantically equal + // states syntactically identical, which is essential for state + // dedup in the emptiness/bisim closure. + expr_ref cr = clean_leaf(r); + if (!re().is_empty(cr)) + result.push_back(get_path_expr(), cr); + return; + } + // Positive branch: c holds. + switch (push(c, false)) { + case l_true: get_cofactors_rec(th, result); break; + case l_undef: get_cofactors_rec(th, result); pop(); break; + case l_false: break; + } + // Negative branch: c does not hold. + switch (push(c, true)) { + case l_true: get_cofactors_rec(el, result); break; + case l_undef: get_cofactors_rec(el, result); pop(); break; + case l_false: break; + } + } + + void derive::get_cofactors(expr* ele, expr* r, expr_ref_pair_vector& result) { + SASSERT(m_util.is_re(r)); + if (ele != m_ele) + reset_op_caches(); + m_ele = ele; + m_trail.push_back(ele); + m_trail.push_back(r); + // Initialize a fresh path/interval context for this traversal. + m_path.reset(); + m_path_stack.reset(); + m_intervals.reset(); + m_intervals.push_back({0u, u().max_char()}); + m_intervals_start = 0; + m_path_expr = m.mk_true(); + get_cofactors_rec(r, result); + } + + void derive::derivative_cofactors(expr* r, expr_ref_pair_vector& result) { + // Compute the symbolic derivative wrt the canonical variable + // (:var 0); operator() sets m_ele to that variable. We use the + // brzozowski normal form (intersections kept above unions, + // deterministic transition regexes) for both the regex_bisim + // equivalence procedure and the emptiness solver. + expr_ref d = (*this)(derivative_kind::brzozowski_t, r); + // Enumerate the reachable, fully ITE-hoisted leaves of the + // transition regex. get_cofactors uses the SAME m_ele set above, + // so the (:var 0) conditions in d are matched and pruned. + get_cofactors(m_ele, d, result); + } + +} + diff --git a/src/ast/rewriter/seq_derive.h b/src/ast/rewriter/seq_derive.h new file mode 100644 index 0000000000..e0559bdf1d --- /dev/null +++ b/src/ast/rewriter/seq_derive.h @@ -0,0 +1,266 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + seq_derive.h + +Abstract: + + Symbolic derivative computation for regular expressions. + Produces an ITE-tree (transition regex) representation where + the free variable is de Bruijn index 0 representing the input character. + + Based on the theory of symbolic derivatives and transition regexes: + - Veanes et al., "On Symbolic Derivatives and Transition Regexes" (LPAR 2024) + - Varatalu, Veanes, Ernits, "RE#" (POPL 2025) + - Stanford, Veanes, Bjørner, "Symbolic Boolean Derivatives" (PLDI 2021) + +Authors: + + Nikolaj Bjorner (nbjorner) 2025-06-03 + +--*/ + +#pragma once + +#include "ast/seq_decl_plugin.h" +#include "ast/arith_decl_plugin.h" +#include "ast/array_decl_plugin.h" +#include "ast/rewriter/bool_rewriter.h" +#include "util/obj_pair_hashtable.h" +#include "util/obj_triple_hashtable.h" +#include + +class seq_rewriter; + +namespace seq { + + enum class derivative_kind { antimirov_t, brzozowski_t }; + /** + * Symbolic derivative engine for regular expressions. + * + * Given a regex r, operator()(r) computes a symbolic derivative δ(r) + * represented as an ITE-tree over character predicates (using de Bruijn + * variable 0 for the character). Evaluating the ITE-tree for a concrete + * character 'a' yields the classical Brzozowski derivative δ_a(r). + * + * The ITE-tree structure implicitly defines minterms (equivalence classes + * of characters indistinguishable by the regex). + * + * Key properties: + * - Results are memoized for termination on cyclic derivative graphs + * - Union/intersection operands are sorted for ACI canonicalization + * - Depth-bounded to prevent stack overflow + */ + class derive { + ast_manager& m; + seq_util m_util; + arith_util m_autil; + bool_rewriter m_br; + seq_rewriter& m_re; + + // Cache: maps (ele, regex) pair to its derivative + obj_pair_map m_acache, m_bcache; + obj_pair_map m_atop_cache, m_btop_cache; // post-simplify cache + expr_ref_vector m_trail; // pin cached results + + // Op cache for ITE-hoisting operations (union, inter, concat, complement) + // Path-aware caches: key is (a, b, path_expr) for binary ops, (a, path_expr) for complement + obj_triple_map m_aunion_cache, m_bunion_cache, m_ainter_cache, m_binter_cache, m_axor_cache, m_bxor_cache; + obj_pair_map m_aconcat_cache, m_bconcat_cache; + obj_pair_map m_acomplement_cache, m_bcomplement_cache; + + // Depth limiting + unsigned m_depth { 0 }; + static const unsigned m_max_depth = 512; + + seq_util::rex& re() { return m_util.re; } + seq_util& u() { return m_util; } + + derivative_kind m_derivative_kind = derivative_kind::antimirov_t; + + // The element (character) for the current derivative computation + expr_ref m_ele; + + // Path state for inline pruning during mk_inter/mk_union/mk_complement + using intervals_t = svector>; + + // Path: vector of signed atoms + svector> m_path; + // Intervals: feasible character ranges under current path (append-only) + intervals_t m_intervals; + unsigned m_intervals_start { 0 }; + // Stack of saved states for push/pop + struct path_save { unsigned path_sz; unsigned intervals_sz; unsigned intervals_start; expr* path_expr; }; + svector m_path_stack; + // Boolean expression encoding of current path (for cache keys) + expr_ref m_path_expr; + + // Path interface + lbool push(expr* c, bool sign); // l_true: implied, l_undef: pushed (must pop), l_false: contradicts + void pop(); // restore state to matching push + expr* get_path_expr() { return m_path_expr; } + + obj_pair_map &cache() { + return m_derivative_kind == derivative_kind::antimirov_t ? m_acache : m_bcache; + } + + obj_pair_map &top_cache() { + return m_derivative_kind == derivative_kind::antimirov_t ? m_atop_cache : m_btop_cache; + } + + obj_triple_map &union_cache() { + return m_derivative_kind == derivative_kind::antimirov_t ? m_aunion_cache : m_bunion_cache; + } + + obj_triple_map &inter_cache() { + return m_derivative_kind == derivative_kind::antimirov_t ? m_ainter_cache : m_binter_cache; + } + + obj_triple_map &xor_cache() { + return m_derivative_kind == derivative_kind::antimirov_t ? m_axor_cache : m_bxor_cache; + } + + obj_pair_map &concat_cache() { + return m_derivative_kind == derivative_kind::antimirov_t ? m_aconcat_cache : m_bconcat_cache; + } + + obj_pair_map &complement_cache() { + return m_derivative_kind == derivative_kind::antimirov_t ? m_acomplement_cache : m_bcomplement_cache; + } + + // Hoist ITE: apply_op through ite(c, t, e) with path pruning + expr_ref apply_ite(expr* c, expr* t, expr* e, expr* r, std::function apply_op); + expr_ref apply_ite(expr* c, expr* t1, expr* e1, expr* t2, expr* e2, std::function apply_op); + expr_ref apply_ite(expr* c, expr* t, expr* e, std::function apply_op); + // Common ITE dispatch for binary ops (union/inter) + expr_ref hoist_ite(expr* a, expr* b, std::function apply_op); + + // Evaluate a condition against the current path/intervals + lbool eval_path_cond(expr* c); + + // Internal helpers for push + lbool push_path_atoms(expr* c, bool sign); + lbool push_intervals_impl(expr* c, bool sign); + + // Core derivative computation + expr_ref derive_rec(expr* r); + expr_ref derive_core(expr* r); + + // Helpers for specific regex constructs + expr_ref derive_to_re(expr* s, sort* seq_sort); + expr_ref derive_range(expr* lo, expr* hi, sort* seq_sort); + expr_ref derive_of_pred(expr* pred, sort* seq_sort); + + // Nullable check: returns a Boolean expression + expr_ref is_nullable(expr* r); + expr_ref is_nullable_symbolic_regex(expr* r, sort* seq_sort); + + // Smart constructors with path-aware simplification and ACI canonicalization + expr_ref mk_union(expr* a, expr* b); + bool are_complements(expr* a, expr* b); + unsigned union_id(expr* e); // complement-aware ID for sorting + bool is_subset(expr* a, expr* b); + expr_ref mk_union_core(expr* a, expr* b); + void add_union_elem(expr_ref_vector& set, expr* e); + expr_ref mk_inter(expr* a, expr* b); + expr_ref mk_inter_core(expr* a, expr* b); + expr_ref mk_concat(expr* a, expr* b); + expr_ref mk_complement(expr* a); + expr_ref mk_complement_core(expr* a); + expr_ref mk_xor(expr *a, expr *b); + expr_ref mk_xor_core(expr *a, expr *b); + expr_ref mk_core(decl_kind k, expr* a, expr* b); + expr_ref mk_ite(expr* c, expr* t, expr* e); + + // Distribute concatenation through ITE/union in derivative + expr_ref mk_deriv_concat(expr* d, expr* tail); + expr_ref mk_deriv_concat_core(expr* d, expr* tail); + + // Extract head character and tail from a sequence expression + bool get_head_tail(expr* s1, expr* s2, expr_ref& hd, expr_ref& tl); + + // Predicate implication for character range conditions. + bool pred_implies(bool sign_a, expr* a, bool sign_b, expr* b); + bool pred_implies(expr* a, expr* b); + + // Normalize reverse(r) + expr_ref mk_regex_reverse(expr* r); + + // Condition evaluation helpers + lbool eval_cond(expr* cond); + lbool eval_range_cond(expr* c); + void intersect_intervals(unsigned lo, unsigned hi); + void exclude_interval(unsigned lo, unsigned hi); + + // Cofactor enumeration over a transition regex (ITE-tree). + void get_cofactors_rec(expr* r, expr_ref_pair_vector& result); + + // Re-apply union/intersection simplifications bottom-up to a cofactor + // leaf. decompose_ite substitutes ITE branch values structurally + // (no simplification), so leaves can contain un-normalized nodes such + // as union(R, none) or inter(R, none); this rebuilds them through + // mk_union/mk_inter so equal states share a canonical form. + expr_ref clean_leaf(expr* r); + + sort* re_sort(expr* r) { return r->get_sort(); } + sort* seq_sort(expr* r) { sort* s = nullptr; m_util.is_re(r, s); return s; } + sort* ele_sort(expr* r) { sort* s = seq_sort(r); sort* e = nullptr; m_util.is_seq(s, e); return e; } + + void reset(); + void reset_op_caches(); + + public: + derive(ast_manager& m, seq_rewriter& re); + + /** + * Compute the derivative of regex r with respect to element ele. + * When ele is a de Bruijn variable, produces a symbolic ITE-tree. + * When ele is a concrete character, produces the concrete derivative. + */ + expr_ref operator()(derivative_kind k, expr* ele, expr* r); + + /** + * Convenience: symbolic derivative using de Bruijn var 0. + */ + expr_ref operator()(derivative_kind k, expr* r); + + /** + * Nullable check: returns a Boolean expression that is true iff r accepts the empty string. + */ + expr_ref nullable(expr* r) { return is_nullable(r); } + + /** + * Enumerate the cofactors (min-terms) of a transition regex r taken with + * respect to element ele. r is an ITE-tree over character predicates on + * ele; for every feasible path through the tree this produces a pair + * (path_condition, leaf_regex). Infeasible character-interval + * combinations are pruned using the same path/interval context that the + * derivative engine uses while hoisting ITEs. + */ + void get_cofactors(expr* ele, expr* r, expr_ref_pair_vector& result); + + /** + * Compute the symbolic derivative of r and enumerate its reachable + * leaves in fully ITE-hoisted normal form. + * + * Concretely this returns, for every feasible minterm (character + * class) of δ(r), a pair (path_condition, target_regex). Every + * if-then-else over the input character (including ones that would + * otherwise be buried under a concat/union) is hoisted to the top + * via the same path/interval pruning used by the derivative engine, + * so each target_regex is free of (:var 0) and its nullability is + * always decidable. Unions are kept intact as single leaves (a + * union leaf denotes a single bisimulation state). Infeasible + * minterms are pruned, so all returned leaves are reachable. + * + * This is the entry point the regex_bisim equivalence procedure + * uses: it consumes the target_regex of each pair and ignores the + * (redundant) path condition. + */ + void derivative_cofactors(expr* r, expr_ref_pair_vector& result); + + }; + +} diff --git a/src/ast/rewriter/seq_range_collapse.cpp b/src/ast/rewriter/seq_range_collapse.cpp new file mode 100644 index 0000000000..8bd725cbeb --- /dev/null +++ b/src/ast/rewriter/seq_range_collapse.cpp @@ -0,0 +1,160 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + seq_range_collapse.cpp + +Abstract: + + Implementation of regex <-> range_predicate translation for the + boolean-combination-of-ranges fragment. See header for the recognized + grammar and the canonical regex AST emitted by materialization. + +Authors: + + Margus Veanes (veanes) 2026 + +--*/ + +#include "ast/rewriter/seq_range_collapse.h" + +namespace seq { + + bool regex_to_range_predicate(seq_util& u, expr* r, range_predicate& out) { + // The range algebra only models sets of single characters over the + // unsigned character domain [0, max_char]. Guard against any regex + // whose element type is not a sequence of characters (e.g. a regex + // over (Seq Int) or (Seq (Seq Char))): for such regexes the + // re.range/re.union/... matchers below would silently fabricate a + // character-class predicate and change semantics. Reject them up + // front so callers fall back to the generic regex path. + sort* seq_sort = nullptr; + if (!u.is_re(r, seq_sort) || !u.is_string(seq_sort)) + return false; + + unsigned const max_char = u.max_char(); + auto& re = u.re; + + if (re.is_empty(r)) { + out = range_predicate::empty(max_char); + return true; + } + if (re.is_full_char(r)) { + out = range_predicate::top(max_char); + return true; + } + unsigned lo = 0, hi = 0; + expr* lo_e = nullptr; + expr* hi_e = nullptr; + if (re.is_range(r, lo_e, hi_e)) { + auto extract_char = [&](expr* e, unsigned& c) -> bool { + if (u.is_const_char(e, c)) return true; + expr* inner = nullptr; + if (u.str.is_unit(e, inner) && u.is_const_char(inner, c)) return true; + zstring s; + if (u.str.is_string(e, s) && s.length() == 1) { + c = s[0]; + return true; + } + return false; + }; + if (!extract_char(lo_e, lo) || !extract_char(hi_e, hi)) + return false; + // Empty/inverted range [lo > hi] is the empty regex. + if (lo > hi) { + out = range_predicate::empty(max_char); + return true; + } + out = range_predicate::range(lo, hi, max_char); + return true; + } + expr *a = nullptr, *b = nullptr, *c = nullptr; + if (re.is_union(r, a, b)) { + range_predicate pa(max_char), pb(max_char); + if (!regex_to_range_predicate(u, a, pa)) return false; + if (!regex_to_range_predicate(u, b, pb)) return false; + out = pa | pb; + return true; + } + auto mk_diff = [&](expr *a, expr *b) -> bool { + range_predicate pa(max_char), pb(max_char); + if (!regex_to_range_predicate(u, a, pa)) + return false; + if (!regex_to_range_predicate(u, b, pb)) + return false; + out = pa - pb; + return true; + }; + if (re.is_diff(r, a, b)) + return mk_diff(a, b); + + if (re.is_intersection(r, a, b) && re.is_complement(b, c)) + return mk_diff(a, c); + + if (re.is_intersection(r, a, b) && re.is_complement(a, c)) + return mk_diff(b, c); + + if (re.is_intersection(r, a, b)) { + range_predicate pa(max_char), pb(max_char); + if (!regex_to_range_predicate(u, a, pa)) return false; + if (!regex_to_range_predicate(u, b, pb)) return false; + out = pa & pb; + return true; + } + + + // NOTE: re.complement is intentionally NOT handled here. + // re.complement is the SEQUENCE-level complement: its language + // includes the empty string, strings of length >= 2, and any + // length-1 string outside the operand. A character-class + // range_predicate can only describe a set of length-1 strings, + // so collapsing re.complement(R) to ~R (character-level + // complement) would change semantics whenever R is wrapped in + // any sequence-level context (e.g. re.diff at the top level, + // or membership tests). De-Morgan equivalences and the + // special cases re.complement(re.empty) / re.complement(re.full) + // are already handled directly in seq_rewriter::mk_re_complement. + return false; + } + + static expr_ref mk_unit_string_from_char(seq_util& u, unsigned c) { + return expr_ref(u.str.mk_string(zstring(c)), u.get_manager()); + } + + static expr_ref mk_single_range_regex(seq_util& u, unsigned lo, unsigned hi, sort* re_sort) { + ast_manager& m = u.get_manager(); + return expr_ref(u.re.mk_range(re_sort, lo, hi), m); + } + + expr_ref range_predicate_to_regex(seq_util& u, range_predicate const& p, sort* seq_sort) { + ast_manager& m = u.get_manager(); + sort* re_sort = u.re.mk_re(seq_sort); + if (p.is_empty()) + return expr_ref(u.re.mk_empty(re_sort), m); + unsigned const n = p.num_ranges(); + SASSERT(n > 0); + if (n == 1) { + auto [lo, hi] = p[0]; + return mk_single_range_regex(u, lo, hi, re_sort); + } + // Build single-range AST nodes first, then sort by expression id + // so the resulting right-associated union matches the canonical + // id-sorted shape that seq_rewriter::merge_regex_sets expects. + // Without this the merge algorithm produces incorrect unions + // when it has to combine our materialized output with another + // (id-sorted) regex set. + expr_ref_vector ranges(m); + for (unsigned i = 0; i < n; ++i) { + auto [lo, hi] = p[i]; + ranges.push_back(mk_single_range_regex(u, lo, hi, re_sort)); + } + std::sort(ranges.data(), ranges.data() + ranges.size(), + [](expr* a, expr* b) { return a->get_id() < b->get_id(); }); + expr_ref acc(ranges.get(n - 1), m); + for (unsigned i = n - 1; i-- > 0; ) + acc = expr_ref(u.re.mk_union(ranges.get(i), acc), m); + return acc; + } + +} diff --git a/src/ast/rewriter/seq_range_collapse.h b/src/ast/rewriter/seq_range_collapse.h new file mode 100644 index 0000000000..16cd5fd67b --- /dev/null +++ b/src/ast/rewriter/seq_range_collapse.h @@ -0,0 +1,71 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + seq_range_collapse.h + +Abstract: + + Recognize regexes that are boolean combinations of character-class + primitives (re.empty, re.full_char, re.range with concrete chars, + and re.union/inter/comp/diff over translatable arguments), and + materialize a seq::range_predicate back into a canonical regex AST. + + Together with seq_rewriter integration, this lets any boolean + combination of character-class regexes collapse to a canonical + multi-range form, so that equivalent character classes share AST + identity, and downstream consumers (derivative, OneStep, caching) + can short-circuit them as pure range predicates. + +Authors: + + Margus Veanes (veanes) 2026 + +--*/ +#pragma once + +#include "ast/rewriter/seq_range_predicate.h" +#include "ast/seq_decl_plugin.h" + +namespace seq { + + /** + * If r is a boolean combination of character-class regex primitives + * over the unsigned character domain [0, max_char], compute the + * equivalent range_predicate and return true. Otherwise return false + * with out untouched. + * + * Recognized fragment (all character-class-preserving operations): + * re.empty -> empty + * re.full_char_set -> top + * re.range "c_lo" "c_hi" (concrete) -> [c_lo, c_hi] + * re.union r1 r2 -> p1 | p2 + * re.intersection r1 r2 -> p1 & p2 + * re.diff r1 r2 -> p1 - p2 + * + * Notably re.complement is NOT recognized: it is a SEQUENCE-level + * complement (over all of Σ*), not a character-class complement, so + * collapsing it would change semantics whenever the result is used + * in any non-character-class context. Sequence-level rewrites for + * re.complement (double-comp, deMorgan, etc.) are handled directly + * in seq_rewriter::mk_re_complement. + */ + bool regex_to_range_predicate(seq_util& u, expr* r, range_predicate& out); + + /** + * Canonical materialization of p as a regex AST over the given + * sequence sort. Two range_predicates with equal canonical + * representations produce structurally identical regex ASTs: + * + * empty -> re.empty + * top -> re.full_char_set + * single range [lo, hi] -> re.range "lo" "hi" + * multiple ranges -> right-associated re.union of single + * ranges, in increasing order of lo + * (matching the canonical range order + * held by range_predicate). + */ + expr_ref range_predicate_to_regex(seq_util& u, range_predicate const& p, sort* seq_sort); + +} diff --git a/src/ast/rewriter/seq_range_predicate.cpp b/src/ast/rewriter/seq_range_predicate.cpp new file mode 100644 index 0000000000..7bb9eac821 --- /dev/null +++ b/src/ast/rewriter/seq_range_predicate.cpp @@ -0,0 +1,292 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + seq_range_predicate.cpp + +Abstract: + + Implementation of the specialized range-algebra used by symbolic + derivative computation and regex rewriting. See seq_range_predicate.h + for the algebraic specification. + + All Boolean operations are implemented as single linear sweeps over + the canonical sorted range vectors and produce canonical output + (sorted, disjoint, non-adjacent). + +Authors: + + Margus Veanes (veanes) 2026 + +--*/ + +#include "ast/rewriter/seq_range_predicate.h" +#include "util/debug.h" +#include +#include + +namespace seq { + + // ----------------------------------------------------------------------- + // Factories + // ----------------------------------------------------------------------- + + range_predicate range_predicate::empty(unsigned max_char) { + return range_predicate(max_char); + } + + range_predicate range_predicate::top(unsigned max_char) { + range_predicate r(max_char); + r.m_ranges.push_back({0u, max_char}); + SASSERT(r.well_formed()); + return r; + } + + range_predicate range_predicate::singleton(unsigned c, unsigned max_char) { + SASSERT(c <= max_char); + range_predicate r(max_char); + r.m_ranges.push_back({c, c}); + SASSERT(r.well_formed()); + return r; + } + + range_predicate range_predicate::range(unsigned lo, unsigned hi, unsigned max_char) { + range_predicate r(max_char); + if (lo <= hi && lo <= max_char) { + unsigned clipped_hi = hi <= max_char ? hi : max_char; + r.m_ranges.push_back({lo, clipped_hi}); + } + SASSERT(r.well_formed()); + return r; + } + + // ----------------------------------------------------------------------- + // Invariants and observers + // ----------------------------------------------------------------------- + + bool range_predicate::well_formed() const { + for (unsigned i = 0; i < m_ranges.size(); ++i) { + auto [lo, hi] = m_ranges[i]; + if (lo > hi) return false; + if (hi > m_max_char) return false; + if (i > 0) { + unsigned prev_hi = m_ranges[i - 1].second; + // Non-adjacent and sorted: prev_hi + 1 < lo, with care + // around prev_hi == UINT_MAX which we never expect because + // hi <= m_max_char. + if (prev_hi + 1 >= lo) return false; + } + } + return true; + } + + bool range_predicate::contains(unsigned c) const { + // Binary search on first element of pairs. + unsigned lo = 0, hi = m_ranges.size(); + while (lo < hi) { + unsigned mid = lo + (hi - lo) / 2; + auto [a, b] = m_ranges[mid]; + if (c < a) hi = mid; + else if (c > b) lo = mid + 1; + else return true; + } + return false; + } + + uint64_t range_predicate::cardinality() const { + uint64_t n = 0; + for (auto [lo, hi] : m_ranges) + n += static_cast(hi) - static_cast(lo) + 1u; + return n; + } + + // ----------------------------------------------------------------------- + // Equality, ordering, hashing + // ----------------------------------------------------------------------- + + bool range_predicate::equals(range_predicate const& o) const { + if (m_max_char != o.m_max_char) return false; + if (m_ranges.size() != o.m_ranges.size()) return false; + for (unsigned i = 0; i < m_ranges.size(); ++i) + if (m_ranges[i] != o.m_ranges[i]) return false; + return true; + } + + bool range_predicate::operator<(range_predicate const& o) const { + if (m_max_char != o.m_max_char) + return m_max_char < o.m_max_char; + unsigned n = std::min(m_ranges.size(), o.m_ranges.size()); + for (unsigned i = 0; i < n; ++i) { + auto a = m_ranges[i]; + auto b = o.m_ranges[i]; + if (a.first != b.first) return a.first < b.first; + if (a.second != b.second) return a.second < b.second; + } + return m_ranges.size() < o.m_ranges.size(); + } + + unsigned range_predicate::hash() const { + // FNV-1a 32-bit over (max_char, then each (lo, hi)). + uint32_t h = 2166136261u; + auto step = [&](uint32_t x) { + h ^= x; + h *= 16777619u; + }; + step(m_max_char); + for (auto [lo, hi] : m_ranges) { + step(lo); + step(hi); + } + return h; + } + + // ----------------------------------------------------------------------- + // Boolean operations + // ----------------------------------------------------------------------- + + namespace { + // Append (lo, hi) to result, merging with the previous range if + // adjacent or overlapping. Maintains canonical form. + inline void append_merged(svector>& result, + unsigned lo, unsigned hi) { + SASSERT(lo <= hi); + if (!result.empty() && result.back().second + 1 >= lo) { + if (result.back().second < hi) + result.back().second = hi; + } else { + result.push_back({lo, hi}); + } + } + } + + range_predicate range_predicate::operator|(range_predicate const& o) const { + SASSERT(m_max_char == o.m_max_char); + range_predicate r(m_max_char); + unsigned i = 0, j = 0; + const unsigned n = m_ranges.size(); + const unsigned m = o.m_ranges.size(); + while (i < n && j < m) { + auto a = m_ranges[i]; + auto b = o.m_ranges[j]; + if (a.first <= b.first) { + append_merged(r.m_ranges, a.first, a.second); + ++i; + } else { + append_merged(r.m_ranges, b.first, b.second); + ++j; + } + } + while (i < n) { + auto a = m_ranges[i++]; + append_merged(r.m_ranges, a.first, a.second); + } + while (j < m) { + auto b = o.m_ranges[j++]; + append_merged(r.m_ranges, b.first, b.second); + } + SASSERT(r.well_formed()); + return r; + } + + range_predicate range_predicate::operator&(range_predicate const& o) const { + SASSERT(m_max_char == o.m_max_char); + range_predicate r(m_max_char); + unsigned i = 0, j = 0; + const unsigned n = m_ranges.size(); + const unsigned m = o.m_ranges.size(); + while (i < n && j < m) { + auto [a_lo, a_hi] = m_ranges[i]; + auto [b_lo, b_hi] = o.m_ranges[j]; + unsigned lo = std::max(a_lo, b_lo); + unsigned hi = std::min(a_hi, b_hi); + if (lo <= hi) + r.m_ranges.push_back({lo, hi}); + // Advance the range that ends first. + if (a_hi < b_hi) ++i; + else if (b_hi < a_hi) ++j; + else { ++i; ++j; } + } + SASSERT(r.well_formed()); + return r; + } + + range_predicate range_predicate::operator~() const { + range_predicate r(m_max_char); + unsigned cursor = 0; + for (auto [lo, hi] : m_ranges) { + if (cursor < lo) + r.m_ranges.push_back({cursor, lo - 1}); + // Step past hi without overflow: hi <= m_max_char and we + // only step when more characters remain. + if (hi >= m_max_char) { + cursor = m_max_char + 1; // sentinel: no more characters + break; + } + cursor = hi + 1; + } + if (cursor <= m_max_char) + r.m_ranges.push_back({cursor, m_max_char}); + SASSERT(r.well_formed()); + return r; + } + + range_predicate range_predicate::operator-(range_predicate const& o) const { + SASSERT(m_max_char == o.m_max_char); + // A - B by linear sweep: for each range of A, subtract overlapping + // ranges of B. Both inputs are sorted so we advance j monotonically. + range_predicate r(m_max_char); + unsigned j = 0; + const unsigned m = o.m_ranges.size(); + for (auto [a_lo, a_hi] : m_ranges) { + unsigned cursor = a_lo; + while (j < m && o.m_ranges[j].second < cursor) + ++j; + unsigned k = j; + while (k < m && o.m_ranges[k].first <= a_hi) { + auto [b_lo, b_hi] = o.m_ranges[k]; + if (cursor < b_lo) + r.m_ranges.push_back({cursor, std::min(a_hi, b_lo - 1)}); + if (b_hi >= a_hi) { + cursor = a_hi + 1; + break; + } + cursor = b_hi + 1; + ++k; + } + if (cursor <= a_hi) + r.m_ranges.push_back({cursor, a_hi}); + } + SASSERT(r.well_formed()); + return r; + } + + range_predicate range_predicate::operator^(range_predicate const& o) const { + SASSERT(m_max_char == o.m_max_char); + // (A | B) - (A & B), but implemented directly with one linear sweep + // over the union of breakpoints. + return (*this | o) - (*this & o); + } + + // ----------------------------------------------------------------------- + // Display + // ----------------------------------------------------------------------- + + std::ostream& range_predicate::display(std::ostream& out) const { + if (m_ranges.empty()) { + return out << "[]"; + } + out << "["; + bool first = true; + for (auto [lo, hi] : m_ranges) { + if (!first) out << ","; + first = false; + if (lo == hi) + out << lo; + else + out << lo << "-" << hi; + } + return out << "]"; + } + +} diff --git a/src/ast/rewriter/seq_range_predicate.h b/src/ast/rewriter/seq_range_predicate.h new file mode 100644 index 0000000000..4fbf4938f5 --- /dev/null +++ b/src/ast/rewriter/seq_range_predicate.h @@ -0,0 +1,127 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + seq_range_predicate.h + +Abstract: + + Specialized range-algebra over an unsigned character domain [0, max_char]. + + A range_predicate represents a subset of the character domain as a + sorted sequence of non-overlapping, non-adjacent, non-empty ranges: + + [(lo_0, hi_0), (lo_1, hi_1), ...] with hi_i + 1 < lo_{i+1}. + + The representation is canonical, so two range_predicates over the same + domain are extensionally equivalent iff their internal vectors are + elementwise equal. + + All Boolean operations (union, intersection, complement, difference) + are linear in the total number of ranges and produce the canonical + representation. + + Intended use: + * path conditions for symbolic derivative computation, + * OneStep predicates capturing length-1 acceptance, + * smart-constructor side conditions for regex rewrites such as + R & psi --> toregex(OneStep(R) & psi). + + The type is a pure value: no ast_manager allocation occurs in its + construction or its Boolean operations. Conversion to and from + expr* is the responsibility of a separate translator (see callers + in seq_derive / seq_rewriter). + +Authors: + + Margus Veanes (veanes) 2026 + +--*/ +#pragma once + +#include "util/vector.h" +#include +#include + +namespace seq { + + class range_predicate { + using range_t = std::pair; + using ranges_t = svector; + + // Sorted by first; ranges are disjoint and non-adjacent; + // every range satisfies lo <= hi <= m_max_char. + ranges_t m_ranges; + unsigned m_max_char { 0 }; + + // Invariant check used in debug builds. + bool well_formed() const; + + public: + range_predicate() = default; + explicit range_predicate(unsigned max_char) : m_max_char(max_char) {} + + // ---------------- Factory functions ---------------- + + static range_predicate empty(unsigned max_char); + static range_predicate top(unsigned max_char); + static range_predicate singleton(unsigned c, unsigned max_char); + static range_predicate range(unsigned lo, unsigned hi, unsigned max_char); + + // ---------------- Observers ---------------- + + unsigned max_char() const { return m_max_char; } + unsigned num_ranges() const { return m_ranges.size(); } + range_t operator[](unsigned i) const { return m_ranges[i]; } + ranges_t const& ranges() const { return m_ranges; } + + bool is_empty() const { return m_ranges.empty(); } + bool is_top() const { + return m_ranges.size() == 1 + && m_ranges[0].first == 0 + && m_ranges[0].second == m_max_char; + } + bool is_singleton(unsigned& c) const { + if (m_ranges.size() != 1) return false; + if (m_ranges[0].first != m_ranges[0].second) return false; + c = m_ranges[0].first; + return true; + } + bool contains(unsigned c) const; + + // Number of characters in the predicate (well-defined for any domain). + uint64_t cardinality() const; + + // ---------------- Equality, ordering, hashing ---------------- + + bool equals(range_predicate const& o) const; + bool operator==(range_predicate const& o) const { return equals(o); } + bool operator!=(range_predicate const& o) const { return !equals(o); } + + // Total order: lexicographic on the canonical range sequence, + // with shorter sequences ordered before longer prefixes. + // Predicates over different domains compare by max_char first. + bool operator<(range_predicate const& o) const; + bool less_than(range_predicate const& o) const { return *this < o; } + + unsigned hash() const; + + // ---------------- Boolean operations ---------------- + + range_predicate operator|(range_predicate const& o) const; // union + range_predicate operator&(range_predicate const& o) const; // intersection + range_predicate operator-(range_predicate const& o) const; // difference + range_predicate operator^(range_predicate const& o) const; // symmetric diff + range_predicate operator~() const; // complement + + // ---------------- Display ---------------- + + std::ostream& display(std::ostream& out) const; + }; + + inline std::ostream& operator<<(std::ostream& out, range_predicate const& p) { + return p.display(out); + } + +} diff --git a/src/ast/rewriter/seq_regex_bisim.cpp b/src/ast/rewriter/seq_regex_bisim.cpp index 5e849017c7..68b2907a55 100644 --- a/src/ast/rewriter/seq_regex_bisim.cpp +++ b/src/ast/rewriter/seq_regex_bisim.cpp @@ -85,41 +85,6 @@ namespace seq { return is_ground(r); } - /* - Collect the leaves of a t-regex der (an ITE / antimirov union / - union-tree with regex leaves) into the output vector. Empty - (re.empty) leaves are dropped. - - Returns false if we encountered an unexpected node (e.g. a free - variable creeping in) — in that case the caller should bail out. - */ - bool regex_bisim::collect_leaves(expr* der, expr_ref_vector& leaves) { - ptr_vector work; - obj_hashtable seen; - work.push_back(der); - seen.insert(der); - while (!work.empty()) { - expr* e = work.back(); - work.pop_back(); - expr* c = nullptr, * t = nullptr, * f = nullptr; - if (m.is_ite(e, c, t, f) || - m_util.re.is_union(e, t, f) || - m_util.re.is_antimirov_union(e, t, f)) { - if (seen.insert_if_not_there(t)) - work.push_back(t); - if (seen.insert_if_not_there(f)) - work.push_back(f); - continue; - } - if (m_util.re.is_empty(e)) - continue; - if (!m_util.is_re(e)) - return false; - leaves.push_back(e); - } - return true; - } - /* Fast inequivalence check based on the get_info().classical flag. @@ -228,15 +193,19 @@ namespace seq { m_worklist.pop_back(); // Compute the symbolic derivative wrt the canonical variable - // (:var 0). The result is a transition regex (ITE tree) whose - // leaves are regex expressions. We use the classical Brzozowski - // entry point so the derivative stays as a single TRegex and - // does not lift unions to the top via antimirov nodes — this - // preserves the XOR-pair invariant the bisimulation relies on. - expr_ref d(m_rw.mk_brz_derivative(r), m); + // (:var 0) and enumerate its reachable leaves in fully + // ITE-hoisted normal form. Every if-then-else over the input + // character — even one that would otherwise be buried under a + // concat or union — is hoisted to the top and infeasible + // minterms are pruned, so each leaf is a ground regex free of + // (:var 0) whose nullability is always decidable. Unions are + // kept intact as single leaves (a union leaf denotes a single + // bisimulation state, never a split into separate states). + expr_ref_pair_vector cofs(m); + m_rw.brz_derivative_cofactors(r, cofs); expr_ref_vector leaves(m); - if (!collect_leaves(d, leaves)) - return l_undef; + for (auto const& p : cofs) + leaves.push_back(p.second); // First pass: check for any nullable leaf (definitive // distinguishing empty-continuation word) or any classically diff --git a/src/ast/rewriter/seq_regex_bisim.h b/src/ast/rewriter/seq_regex_bisim.h index d158cc3793..7ec5c30a33 100644 --- a/src/ast/rewriter/seq_regex_bisim.h +++ b/src/ast/rewriter/seq_regex_bisim.h @@ -74,7 +74,6 @@ namespace seq { unsigned node_of(expr* r); bool merge_leaf(expr* xor_pair); - bool collect_leaves(expr* der, expr_ref_vector& leaves); lbool nullability(expr* r); bool is_supported(expr* r); // Returns true if the leaf l proves that the original pair is diff --git a/src/ast/rewriter/seq_rewriter.cpp b/src/ast/rewriter/seq_rewriter.cpp index a995ec673b..ca0038c487 100644 --- a/src/ast/rewriter/seq_rewriter.cpp +++ b/src/ast/rewriter/seq_rewriter.cpp @@ -21,12 +21,12 @@ Authors: #include "util/uint_set.h" #include "ast/rewriter/seq_rewriter.h" #include "ast/rewriter/seq_regex_bisim.h" +#include "ast/rewriter/seq_range_collapse.h" #include "ast/arith_decl_plugin.h" #include "ast/array_decl_plugin.h" #include "ast/ast_pp.h" #include "ast/ast_ll_pp.h" #include "ast/ast_util.h" -#include "ast/for_each_expr.h" #include "ast/well_sorted.h" #include "ast/rewriter/var_subst.h" #include "ast/rewriter/expr_safe_replace.h" @@ -241,12 +241,6 @@ br_status seq_rewriter::mk_app_core(func_decl * f, unsigned num_args, expr * con st = mk_re_concat(args[0], args[1], result); } break; - case _OP_RE_ANTIMIROV_UNION: - SASSERT(num_args == 2); - // Rewrite antimirov union to real union - result = re().mk_union(args[0], args[1]); - st = BR_REWRITE1; - break; case OP_RE_UNION: if (num_args == 1) { result = args[0]; @@ -500,6 +494,7 @@ br_status seq_rewriter::mk_seq_concat(expr* a, expr* b, expr_ref& result) { expr* c, *d; bool isc1 = str().is_string(a, s1) && m_coalesce_chars; bool isc2 = str().is_string(b, s2) && m_coalesce_chars; + if (isc1 && isc2) { result = str().mk_string(s1 + s2); return BR_DONE; @@ -1856,6 +1851,127 @@ br_status seq_rewriter::mk_seq_replace_all(expr* a, expr* b, expr* c, expr_ref& return BR_FAILED; } + +/** + * replace_char("ab", "a", b") = empty + * replace_char("bc", "a", b") = {"a", "b"}"c" + * replace_char(R u R', "a", "b") = replace_char(R, "a", "b") u replace_char(R', "a", "b") + * replace_char(R n R', "a", "b") = replace_char(R, "a", "b") n replace_char(R', "a", "b") + * replace_char(R*, "a", "b") = replace_char(R, "a", "b")* + * replace_char(R R', "a", "b") = replace_char(R, "a", "b") replace_char(R', "a", "b") + */ +expr_ref seq_rewriter::re_replace_char(expr *r, unsigned a_ch, unsigned b_ch, expr *a_str, expr *b_str) { + expr *r1 = nullptr, *r2 = nullptr, *s = nullptr; + zstring str_val; + sort *seq_sort = nullptr; + + if (re().is_to_re(r, s) && str().is_string(s, str_val)) { + seq_sort = s->get_sort(); + expr_ref_vector parts(m()); + for (unsigned i = 0; i < str_val.length(); ++i) { + if (str_val[i] == a_ch) { + // replace_all never outputs a_ch, so this position is impossible + return expr_ref(re().mk_empty(re().mk_re(seq_sort)), m()); + } + else if (str_val[i] == b_ch) { + // b in output came from either a or b in x + auto a_re = re().mk_to_re(a_str); + auto b_re = re().mk_to_re(b_str); + parts.push_back(re().mk_union(a_re, b_re)); + } + else { + zstring ch(str_val[i]); + parts.push_back(re().mk_to_re(str().mk_string(ch))); + } + } + if (parts.empty()) + return expr_ref(re().mk_epsilon(seq_sort), m()); + expr_ref result(parts.back(), m()); + for (int i = parts.size() - 1; i-- > 0;) + result = re().mk_concat(parts.get(i), result); + return result; + } + + if (re().is_range(r, r1, r2)) { + zstring lo_s, hi_s; + if (str().is_string(r1, lo_s) && str().is_string(r2, hi_s) && lo_s.length() == 1 && hi_s.length() == 1) { + unsigned lo = lo_s[0], hi = hi_s[0]; + // Build the transformed range: + // - Remove a_ch from the range (impossible in output) + // - Replace b_ch with union(a_str, b_str) + expr_ref_vector parts(m()); + // Characters in [lo, hi] excluding a_ch and b_ch + if (lo <= hi) { + // Sub-ranges excluding a_ch and b_ch + unsigned prev = lo; + for (unsigned ch = lo; ch <= hi; ++ch) { + if (ch == a_ch || ch == b_ch) { + if (prev < ch) { + zstring prev_z(prev), pred_z(ch - 1); + parts.push_back(re().mk_range(str().mk_string(prev_z), str().mk_string(pred_z))); + } + if (ch == b_ch) { + parts.push_back(re().mk_union(re().mk_to_re(a_str), re().mk_to_re(b_str))); + } + // a_ch is simply excluded (not added) + prev = ch + 1; + } + } + if (prev <= hi) { + zstring prev_z(prev), hi_z(hi); + parts.push_back(re().mk_range(str().mk_string(prev_z), str().mk_string(hi_z))); + } + } + if (parts.empty()) { + sort *re_sort = r->get_sort(); + return expr_ref(re().mk_empty(re_sort), m()); + } + expr_ref result(parts[0].get(), m()); + for (unsigned i = 1; i < parts.size(); ++i) + result = re().mk_union(result, parts[i].get()); + return result; + } + return expr_ref(r, m()); + } + + if (re().is_union(r, r1, r2)) { + return expr_ref( + re().mk_union(re_replace_char(r1, a_ch, b_ch, a_str, b_str), re_replace_char(r2, a_ch, b_ch, a_str, b_str)), + m()); + } + if (re().is_intersection(r, r1, r2)) { + return expr_ref( + re().mk_inter(re_replace_char(r1, a_ch, b_ch, a_str, b_str), re_replace_char(r2, a_ch, b_ch, a_str, b_str)), + m()); + } + if (re().is_concat(r, r1, r2)) { + return expr_ref(re().mk_concat(re_replace_char(r1, a_ch, b_ch, a_str, b_str), + re_replace_char(r2, a_ch, b_ch, a_str, b_str)), + m()); + } + if (re().is_star(r, r1)) { + return expr_ref(re().mk_star(re_replace_char(r1, a_ch, b_ch, a_str, b_str)), m()); + } + if (re().is_plus(r, r1)) { + return expr_ref(re().mk_plus(re_replace_char(r1, a_ch, b_ch, a_str, b_str)), m()); + } + if (re().is_opt(r, r1)) { + return expr_ref(re().mk_opt(re_replace_char(r1, a_ch, b_ch, a_str, b_str)), m()); + } + unsigned lo, hi; + if (re().is_loop(r, r1, lo, hi)) { + return expr_ref(re().mk_loop(re_replace_char(r1, a_ch, b_ch, a_str, b_str), lo, hi), m()); + } + if (re().is_loop(r, r1, lo)) { + return expr_ref(re().mk_loop(re_replace_char(r1, a_ch, b_ch, a_str, b_str), lo), m()); + } + if (re().is_complement(r)) { + UNREACHABLE(); + } + // For anything else (full_seq, empty, epsilon, of_pred, etc.), return unchanged + return expr_ref(r, m()); +} + /** rewrites for map(f, s): @@ -2606,7 +2722,7 @@ expr_ref seq_rewriter::is_nullable(expr* r) { << mk_pp(r, m()) << std::endl;); expr_ref result(m_op_cache.find(_OP_RE_IS_NULLABLE, r, nullptr, nullptr), m()); if (!result) { - result = is_nullable_rec(r); + result = m_derive.nullable(r); m_op_cache.insert(_OP_RE_IS_NULLABLE, r, nullptr, nullptr, result); } STRACE(seq_verbose, tout << "is_nullable result: " @@ -2614,117 +2730,6 @@ expr_ref seq_rewriter::is_nullable(expr* r) { return result; } -expr_ref seq_rewriter::is_nullable_rec(expr* r) { - SASSERT(m_util.is_re(r) || m_util.is_seq(r)); - expr* r1 = nullptr, *r2 = nullptr, *cond = nullptr; - sort* seq_sort = nullptr; - unsigned lo = 0, hi = 0; - zstring s1; - expr_ref result(m()); - if (re().is_concat(r, r1, r2) || - re().is_intersection(r, r1, r2)) { - m_br.mk_and(is_nullable(r1), is_nullable(r2), result); - } - else if (re().is_union(r, r1, r2) || re().is_antimirov_union(r, r1, r2)) { - m_br.mk_or(is_nullable(r1), is_nullable(r2), result); - } - else if (re().is_diff(r, r1, r2)) { - m_br.mk_not(is_nullable(r2), result); - m_br.mk_and(result, is_nullable(r1), result); - } - else if (re().is_xor(r, r1, r2)) { - // Null(r1 XOR r2) = Null(r1) XOR Null(r2) - expr_ref n1(is_nullable(r1), m()); - expr_ref n2(is_nullable(r2), m()); - // Simplify when either operand is a boolean literal so the - // bisimulation procedure can use the answer directly. - if (m().is_true(n1)) - result = mk_not(m(), n2); - else if (m().is_false(n1)) - result = n2; - else if (m().is_true(n2)) - result = mk_not(m(), n1); - else if (m().is_false(n2)) - result = n1; - else - result = m().mk_xor(n1, n2); - } - else if (re().is_star(r) || - re().is_opt(r) || - re().is_full_seq(r) || - re().is_epsilon(r) || - (re().is_loop(r, r1, lo) && lo == 0) || - (re().is_loop(r, r1, lo, hi) && lo == 0)) { - result = m().mk_true(); - } - else if (re().is_full_char(r) || - re().is_empty(r) || - re().is_of_pred(r) || - re().is_range(r)) { - result = m().mk_false(); - } - else if (re().is_plus(r, r1) || - (re().is_loop(r, r1, lo) && lo > 0) || - (re().is_loop(r, r1, lo, hi) && lo > 0) || - (re().is_reverse(r, r1))) { - result = is_nullable(r1); - } - else if (re().is_complement(r, r1)) { - m_br.mk_not(is_nullable(r1), result); - } - else if (re().is_to_re(r, r1)) { - result = is_nullable(r1); - } - else if (m().is_ite(r, cond, r1, r2)) { - m_br.mk_ite(cond, is_nullable(r1), is_nullable(r2), result); - } - else if (m_util.is_re(r, seq_sort)) { - result = is_nullable_symbolic_regex(r, seq_sort); - } - else if (str().is_concat(r, r1, r2)) { - m_br.mk_and(is_nullable(r1), is_nullable(r2), result); - } - else if (str().is_empty(r)) { - result = m().mk_true(); - } - else if (str().is_unit(r)) { - result = m().mk_false(); - } - else if (str().is_string(r, s1)) { - result = m().mk_bool_val(s1.length() == 0); - } - else { - SASSERT(m_util.is_seq(r)); - result = m().mk_eq(str().mk_empty(r->get_sort()), r); - } - return result; -} - -expr_ref seq_rewriter::is_nullable_symbolic_regex(expr* r, sort* seq_sort) { - SASSERT(m_util.is_re(r)); - expr* elem = nullptr, *r1 = r, * r2 = nullptr, * s = nullptr; - expr_ref elems(str().mk_empty(seq_sort), m()); - expr_ref result(m()); - while (re().is_derivative(r1, elem, r2)) { - if (str().is_empty(elems)) - elems = str().mk_unit(elem); - else - elems = str().mk_concat(str().mk_unit(elem), elems); - r1 = r2; - } - if (re().is_to_re(r1, s)) { - // r is nullable - // iff after taking the derivatives the remaining sequence is empty - // iff the inner sequence equals to the sequence of derivative elements in reverse - result = m().mk_eq(elems, s); - return result; - } - // the default case when either r is not a derivative - // or when the nested derivatives are not applied to a sequence - result = re().mk_in_re(str().mk_empty(seq_sort), r); - return result; -} - /* Push reverse inwards (whenever possible). */ @@ -2827,10 +2832,6 @@ br_status seq_rewriter::mk_re_reverse(expr* r, expr_ref& result) { } } -/*************************************************** - ***** Begin Derivative Code ***** - ***************************************************/ - /* Symbolic derivative: seq -> regex -> regex seq should be single char @@ -2854,489 +2855,37 @@ br_status seq_rewriter::mk_re_derivative(expr* ele, expr* r, expr_ref& result) { return BR_DONE; } -/* - Note: Derivative Normal Form - - When computing derivatives recursively, we preserve the following - BDD normal form: - - - At the top level, the derivative is a union of antimirov derivatives - (Conceptually each element of the union is a different derivative). - We currently express this derivative using an internal op code: - _OP_RE_antimirov_UNION - - An antimirov derivative is a nested if-then-else term. - if-then-elses are pushed outwards and sorted by condition ID - (cond->get_id()), from largest on the outside to smallest on the - inside. Duplicate nested conditions are eliminated. - - The leaves of the if-then-else BDD can have unions themselves, - but these are interpreted as Regex union, not as separate antimirov - derivatives. - - To debug the normal form, call Z3 with -dbg:seq_regex: - this calls check_deriv_normal_form (below) periodically. - - The main logic is in mk_der_op_rec for combining normal forms - (some also in mk_der_compl_rec). -*/ - -#ifdef Z3DEBUG -/* - Debugging to check the derivative normal form that we assume - (see definition above). - - This may fail on unusual/unexpected REs, such as those containing - regex variables, but this is by design as this is only checked - during debugging, and we have not considered how normal form - should apply in such cases. -*/ -bool seq_rewriter::check_deriv_normal_form(expr* r, int level) { - if (level == 3) { // top level - STRACE(seq_verbose, tout - << "Checking derivative normal form invariant...";); - } - expr *r1 = nullptr, *r2 = nullptr, *p = nullptr, *s = nullptr; - unsigned lo = 0, hi = 0; - STRACE(seq_verbose, tout << " (level " << level << ")";); - int new_level = 0; - if (re().is_antimirov_union(r)) { - SASSERT(level >= 2); - new_level = 2; - } - else if (m().is_ite(r)) { - SASSERT(level >= 1); - new_level = 1; - } - - SASSERT(!re().is_diff(r)); - SASSERT(!re().is_opt(r)); - SASSERT(!re().is_plus(r)); - - if (re().is_antimirov_union(r, r1, r2) || - re().is_concat(r, r1, r2) || - re().is_union(r, r1, r2) || - re().is_intersection(r, r1, r2) || - re().is_xor(r, r1, r2) || - m().is_ite(r, p, r1, r2)) { - check_deriv_normal_form(r1, new_level); - check_deriv_normal_form(r2, new_level); - } - else if (re().is_star(r, r1) || - re().is_complement(r, r1) || - re().is_loop(r, r1, lo) || - re().is_loop(r, r1, lo, hi)) { - check_deriv_normal_form(r1, new_level); - } - else if (re().is_reverse(r, r1)) { - SASSERT(re().is_to_re(r1)); - } - else if (re().is_full_seq(r) || - re().is_empty(r) || - re().is_range(r) || - re().is_full_char(r) || - re().is_of_pred(r) || - re().is_to_re(r, s)) { - // OK - } - else { - SASSERT(false); - } - if (level == 3) { - STRACE(seq_verbose, tout << " passed!" << std::endl;); - } - return true; -} -#endif expr_ref seq_rewriter::mk_derivative(expr* r) { - sort* seq_sort = nullptr, * ele_sort = nullptr; - VERIFY(m_util.is_re(r, seq_sort)); - VERIFY(m_util.is_seq(seq_sort, ele_sort)); - expr_ref v(m().mk_var(0, ele_sort), m()); - return mk_antimirov_deriv(v, r, m().mk_true()); -} - -expr_ref seq_rewriter::mk_brz_derivative(expr* r) { - sort* seq_sort = nullptr, * ele_sort = nullptr; - VERIFY(m_util.is_re(r, seq_sort)); - VERIFY(m_util.is_seq(seq_sort, ele_sort)); - expr_ref v(m().mk_var(0, ele_sort), m()); - return mk_derivative_rec(v, r); + auto result = m_derive(seq::derivative_kind::antimirov_t, r); + TRACE(seq, tout << "Derivative of " << mk_pp(r, m()) << "\nis\n" << result << std::endl;); + return result; } expr_ref seq_rewriter::mk_derivative(expr* ele, expr* r) { - return mk_antimirov_deriv(ele, r, m().mk_true()); -} - -expr_ref seq_rewriter::mk_antimirov_deriv(expr* e, expr* r, expr* path) { - // Ensure references are owned - expr_ref _e(e, m()), _path(path, m()), _r(r, m()); - expr_ref result(m_op_cache.find(OP_RE_DERIVATIVE, e, r, path), m()); - if (!result) { - mk_antimirov_deriv_rec(e, r, path, result); - m_op_cache.insert(OP_RE_DERIVATIVE, e, r, path, result); - STRACE(seq_regex, tout << "D(" << mk_pp(e, m()) << "," << mk_pp(r, m()) << "," << mk_pp(path, m()) << ")" << std::endl;); - STRACE(seq_regex, tout << "= " << mk_pp(result, m()) << std::endl;); - } + auto result = m_derive(seq::derivative_kind::antimirov_t, ele, r); + TRACE(seq, + tout << "Derivative of " << mk_pp(r, m()) << " w.r.t. " << mk_pp(ele, m()) << "\nis\n" << result << std::endl;); return result; } -void seq_rewriter::mk_antimirov_deriv_rec(expr* e, expr* r, expr* path, expr_ref& result) { - sort* seq_sort = nullptr, * ele_sort = nullptr; - expr_ref _r(r, m()), _path(path, m()); - VERIFY(m_util.is_re(r, seq_sort)); - VERIFY(m_util.is_seq(seq_sort, ele_sort)); - SASSERT(ele_sort == e->get_sort()); - expr* r1 = nullptr, * r2 = nullptr, * c = nullptr; - expr_ref c1(m()); - expr_ref c2(m()); - auto nothing = [&]() { return expr_ref(re().mk_empty(r->get_sort()), m()); }; - auto epsilon = [&]() { return expr_ref(re().mk_epsilon(seq_sort), m()); }; - auto dotstar = [&]() { return expr_ref(re().mk_full_seq(r->get_sort()), m()); }; - unsigned lo = 0, hi = 0; - if (re().is_empty(r) || re().is_epsilon(r)) - // D(e,[]) = D(e,()) = [] - result = nothing(); - else if (re().is_full_seq(r) || re().is_dot_plus(r)) - // D(e,.*) = D(e,.+) = .* - result = dotstar(); - else if (re().is_full_char(r)) - // D(e,.) = () - result = epsilon(); - else if (re().is_to_re(r, r1)) { - expr_ref h(m()); - expr_ref t(m()); - // here r1 is a sequence - if (get_head_tail(r1, h, t)) { - if (eq_char(e, h)) - result = re().mk_to_re(t); - else if (neq_char(e, h)) - result = nothing(); - else - result = re().mk_ite_simplify(m().mk_eq(e, h), re().mk_to_re(t), nothing()); - } - else { - // observe that the precondition |r1|>0 is is implied by c1 for use of mk_seq_first - { - auto is_non_empty = m().mk_not(m().mk_eq(r1, str().mk_empty(seq_sort))); - auto eq_first = m().mk_eq(mk_seq_first(r1), e); - m_br.mk_and(is_non_empty, eq_first, c1); - } - m_br.mk_and(path, c1, c2); - if (m().is_false(c2)) - result = nothing(); - else - // observe that the precondition |r1|>0 is implied by c1 for use of mk_seq_rest - result = m().mk_ite(c1, re().mk_to_re(mk_seq_rest(r1)), nothing()); - } - } - else if (re().is_reverse(r, r2)) - if (re().is_to_re(r2, r1)) { - // here r1 is a sequence - // observe that the precondition |r1|>0 of mk_seq_last is implied by c1 - { - auto is_non_empty = m().mk_not(m().mk_eq(r1, str().mk_empty(seq_sort))); - auto eq_last = m().mk_eq(mk_seq_last(r1), e); - m_br.mk_and(is_non_empty, eq_last, c1); - } - m_br.mk_and(path, c1, c2); - if (m().is_false(c2)) - result = nothing(); - else - // observe that the precondition |r1|>0 of mk_seq_rest is implied by c1 - result = re().mk_ite_simplify(c1, re().mk_reverse(re().mk_to_re(mk_seq_butlast(r1))), nothing()); - } - else { - result = mk_regex_reverse(r2); - if (result.get() == r) - //r2 is an uninterpreted regex that is stuck - //for example if r = (re.reverse R) where R is a regex variable then - //here result.get() == r - result = re().mk_derivative(e, result); - else - result = mk_antimirov_deriv(e, result, path); - } - else if (re().is_concat(r, r1, r2)) { - expr_ref r1nullable(is_nullable(r1), m()); - c1 = mk_antimirov_deriv_concat(mk_antimirov_deriv(e, r1, path), r2); - expr_ref r1nullable_and_path(m()); - m_br.mk_and(r1nullable, path, r1nullable_and_path); - if (m().is_false(r1nullable_and_path)) - // D(e,r1)r2 - result = c1; - else - // D(e,r1)r2|(ite (r1nullable) (D(e,r2)) []) - // observe that (mk_ite_simplify(true, D(e,r2), []) = D(e,r2) - result = mk_antimirov_deriv_union(c1, re().mk_ite_simplify(r1nullable, mk_antimirov_deriv(e, r2, path), nothing())); - } - else if (m().is_ite(r, c, r1, r2)) { - { - auto cp = m().mk_and(c, path); - c1 = simplify_path(e, cp); - } - { - auto notc = m().mk_not(c); - auto np = m().mk_and(notc, path); - c2 = simplify_path(e, np); - } - if (m().is_false(c1)) - result = mk_antimirov_deriv(e, r2, c2); - else if (m().is_false(c2)) - result = mk_antimirov_deriv(e, r1, c1); - else - result = re().mk_ite_simplify(c, mk_antimirov_deriv(e, r1, c1), mk_antimirov_deriv(e, r2, c2)); - } - else if (re().is_range(r, r1, r2)) { - expr_ref range(m()); - expr_ref psi(m().mk_false(), m()); - if (str().is_unit_string(r1, c1) && str().is_unit_string(r2, c2)) { - // SASSERT(u().is_char(c1)); - // SASSERT(u().is_char(c2)); - // case: c1 <= e <= c2 - // deterministic evaluation for range bounds - auto a_le = u().mk_le(c1, e); - auto b_le = u().mk_le(e, c2); - auto rng_cond = m().mk_and(a_le, b_le); - range = simplify_path(e, rng_cond); - psi = simplify_path(e, m().mk_and(path, range)); - } - else if (!str().is_string(r1) && str().is_unit_string(r2, c2)) { - SASSERT(u().is_char(c2)); - // r1 nonground: |r1|=1 & r1[0] <= e <= c2 - expr_ref one(m_autil.mk_int(1), m()); - expr_ref zero(m_autil.mk_int(0), m()); - expr_ref r1_length_eq_one(m().mk_eq(str().mk_length(r1), one), m()); - expr_ref r1_0(str().mk_nth_i(r1, zero), m()); - range = simplify_path(e, m().mk_and(r1_length_eq_one, m().mk_and(u().mk_le(r1_0, e), u().mk_le(e, c2)))); - psi = simplify_path(e, m().mk_and(path, range)); - } - else if (!str().is_string(r2) && str().is_unit_string(r1, c1)) { - SASSERT(u().is_char(c1)); - // r2 nonground: |r2|=1 & c1 <= e <= r2_0 - expr_ref one(m_autil.mk_int(1), m()); - expr_ref zero(m_autil.mk_int(0), m()); - expr_ref r2_length_eq_one(m().mk_eq(str().mk_length(r2), one), m()); - expr_ref r2_0(str().mk_nth_i(r2, zero), m()); - range = simplify_path(e, m().mk_and(r2_length_eq_one, m().mk_and(u().mk_le(c1, e), u().mk_le(e, r2_0)))); - psi = simplify_path(e, m().mk_and(path, range)); - } - else if (!str().is_string(r1) && !str().is_string(r2)) { - // both r1 and r2 nonground: |r1|=1 & |r2|=1 & r1[0] <= e <= r2[0] - expr_ref one(m_autil.mk_int(1), m()); - expr_ref zero(m_autil.mk_int(0), m()); - expr_ref r1_length_eq_one(m().mk_eq(str().mk_length(r1), one), m()); - expr_ref r1_0(str().mk_nth_i(r1, zero), m()); - expr_ref r2_length_eq_one(m().mk_eq(str().mk_length(r2), one), m()); - expr_ref r2_0(str().mk_nth_i(r2, zero), m()); - range = simplify_path(e, m().mk_and(r1_length_eq_one, m().mk_and(r2_length_eq_one, m().mk_and(u().mk_le(r1_0, e), u().mk_le(e, r2_0))))); - psi = simplify_path(e, m().mk_and(path, range)); - } - if (m().is_false(psi)) - result = nothing(); - else - result = re().mk_ite_simplify(range, epsilon(), nothing()); - } - else if (re().is_union(r, r1, r2)) - result = mk_antimirov_deriv_union(mk_antimirov_deriv(e, r1, path), mk_antimirov_deriv(e, r2, path)); - else if (re().is_intersection(r, r1, r2)) - result = mk_antimirov_deriv_intersection(e, - mk_antimirov_deriv(e, r1, path), - mk_antimirov_deriv(e, r2, path), m().mk_true()); - else if (re().is_star(r, r1) || re().is_plus(r, r1) || (re().is_loop(r, r1, lo) && 0 <= lo && lo <= 1)) - result = mk_antimirov_deriv_concat(mk_antimirov_deriv(e, r1, path), re().mk_star(r1)); - else if (re().is_loop(r, r1, lo)) - result = mk_antimirov_deriv_concat(mk_antimirov_deriv(e, r1, path), re().mk_loop(r1, lo - 1)); - else if (re().is_loop(r, r1, lo, hi)) { - if ((lo == 0 && hi == 0) || hi < lo) - result = nothing(); - else { - expr_ref t(re().mk_loop_proper(r1, (lo == 0 ? 0 : lo - 1), hi - 1), m()); - result = mk_antimirov_deriv_concat(mk_antimirov_deriv(e, r1, path), t); - } - } - else if (re().is_opt(r, r1)) - result = mk_antimirov_deriv(e, r1, path); - else if (re().is_complement(r, r1)) - // D(e,~r1) = ~D(e,r1) - result = mk_antimirov_deriv_negate(e, mk_antimirov_deriv(e, r1, path)); - else if (re().is_diff(r, r1, r2)) - result = mk_antimirov_deriv_intersection(e, - mk_antimirov_deriv(e, r1, path), - mk_antimirov_deriv_negate(e, mk_antimirov_deriv(e, r2, path)), m().mk_true()); - else if (re().is_xor(r, r1, r2)) - // D(e, r1 XOR r2) = D(e, r1) XOR D(e, r2) - result = mk_der_xor(mk_antimirov_deriv(e, r1, path), - mk_antimirov_deriv(e, r2, path)); - else if (re().is_of_pred(r, r1)) { - array_util array(m()); - expr* args[2] = { r1, e }; - result = array.mk_select(2, args); - // Use mk_der_cond to normalize - result = mk_der_cond(result, e, seq_sort); - } - else - // stuck cases - result = re().mk_derivative(e, r); -} - -expr_ref seq_rewriter::mk_antimirov_deriv_intersection(expr* e, expr* d1, expr* d2, expr* path) { - sort* seq_sort = nullptr, * ele_sort = nullptr; - VERIFY(m_util.is_re(d1, seq_sort)); - VERIFY(m_util.is_seq(seq_sort, ele_sort)); - expr_ref result(m()); - expr* c, * a, * b; - if (re().is_empty(d1)) - result = d1; - else if (re().is_empty(d2)) - result = d2; - else if (m().is_ite(d1, c, a, b)) { - expr_ref path_and_c(simplify_path(e, m().mk_and(path, c)), m()); - expr_ref path_and_notc(simplify_path(e, m().mk_and(path, m().mk_not(c))), m()); - if (m().is_false(path_and_c)) - result = mk_antimirov_deriv_intersection(e, b, d2, path); - else if (m().is_false(path_and_notc)) - result = mk_antimirov_deriv_intersection(e, a, d2, path); - else { - expr_ref th(mk_antimirov_deriv_intersection(e, a, d2, path_and_c), m()); - expr_ref el(mk_antimirov_deriv_intersection(e, b, d2, path_and_notc), m()); - if (th == el) - result = th; - else - result = m().mk_ite(c, th, el); - } - } - else if (m().is_ite(d2)) - // swap d1 and d2 - result = mk_antimirov_deriv_intersection(e, d2, d1, path); - else if (d1 == d2 || re().is_full_seq(d2)) - result = mk_antimirov_deriv_restrict(e, d1, path); - else if (re().is_full_seq(d1)) - result = mk_antimirov_deriv_restrict(e, d2, path); - else if (re().is_union(d1, a, b)) - // distribute intersection over the union in d1 - result = mk_antimirov_deriv_union(mk_antimirov_deriv_intersection(e, a, d2, path), - mk_antimirov_deriv_intersection(e, b, d2, path)); - else if (re().is_union(d2, a, b)) - // distribute intersection over the union in d2 - result = mk_antimirov_deriv_union(mk_antimirov_deriv_intersection(e, d1, a, path), - mk_antimirov_deriv_intersection(e, d1, b, path)); - else - result = mk_regex_inter_normalize(d1, d2); - return result; -} - -expr_ref seq_rewriter::mk_antimirov_deriv_concat(expr* d, expr* r) { - expr_ref result(m()); - expr_ref _r(r, m()), _d(d, m()); - expr* c, * t, * e; - if (m().is_ite(d, c, t, e)) { - auto r2 = mk_antimirov_deriv_concat(e, r); - auto r1 = mk_antimirov_deriv_concat(t, r); - result = m().mk_ite(c, r1, r2); - } - else if (re().is_union(d, t, e)) - result = mk_antimirov_deriv_union(mk_antimirov_deriv_concat(t, r), mk_antimirov_deriv_concat(e, r)); - else - result = mk_re_append(d, r); - SASSERT(result.get()); - return result; -} - -expr_ref seq_rewriter::mk_antimirov_deriv_negate(expr* elem, expr* d) { - sort* seq_sort = nullptr; - VERIFY(m_util.is_re(d, seq_sort)); - auto nothing = [&]() { return expr_ref(re().mk_empty(d->get_sort()), m()); }; - auto epsilon = [&]() { return expr_ref(re().mk_epsilon(seq_sort), m()); }; - auto dotstar = [&]() { return expr_ref(re().mk_full_seq(d->get_sort()), m()); }; - auto dotplus = [&]() { return expr_ref(re().mk_plus(re().mk_full_char(d->get_sort())), m()); }; - expr_ref result(m()); - expr* c, * t, * e; - if (re().is_empty(d)) - result = dotstar(); - else if (re().is_epsilon(d)) - result = dotplus(); - else if (re().is_full_seq(d)) - result = nothing(); - else if (re().is_dot_plus(d)) - result = epsilon(); - else if (m().is_ite(d, c, t, e)) { - expr_ref th(mk_antimirov_deriv_negate(elem, t), m()); - expr_ref el(mk_antimirov_deriv_negate(elem, e), m()); - if (th == el) - result = th; - else - result = m().mk_ite(c, th, el); - } - else if (re().is_union(d, t, e)) - result = mk_antimirov_deriv_intersection(elem, mk_antimirov_deriv_negate(elem, t), mk_antimirov_deriv_negate(elem, e), m().mk_true()); - else if (re().is_intersection(d, t, e)) - result = mk_antimirov_deriv_union(mk_antimirov_deriv_negate(elem, t), mk_antimirov_deriv_negate(elem, e)); - else if (re().is_complement(d, t)) - result = t; - else - result = re().mk_complement(d); - return result; -} - -expr_ref seq_rewriter::mk_antimirov_deriv_union(expr* d1, expr* d2) { - sort* seq_sort = nullptr, * ele_sort = nullptr; - VERIFY(m_util.is_re(d1, seq_sort)); - VERIFY(m_util.is_seq(seq_sort, ele_sort)); - expr_ref result(m()); - expr* c1, * t1, * e1, * c2, * t2, * e2; - if (m().is_ite(d1, c1, t1, e1) && m().is_ite(d2, c2, t2, e2) && c1 == c2) { - expr_ref th(mk_antimirov_deriv_union(t1, t2), m()); - expr_ref el(mk_antimirov_deriv_union(e1, e2), m()); - if (th == el) - result = th; - else - // eliminate duplicate branching on exactly the same condition - result = m().mk_ite(c1, th, el); - } - else - result = mk_regex_union_normalize(d1, d2); - return result; -} - -// restrict the guards of all conditionals id d and simplify the resulting derivative -// restrict(if(c, a, b), cond) = if(c, restrict(a, cond & c), restrict(b, cond & ~c)) -// restrict(a U b, cond) = restrict(a, cond) U restrict(b, cond) -// where {} U X = X, X U X = X -// restrict(R, cond) = R -// -// restrict(d, false) = [] -// -// it is already assumed that the restriction takes place within a branch -// so the condition is not added explicitly but propagated down in order to eliminate -// infeasible cases -expr_ref seq_rewriter::mk_antimirov_deriv_restrict(expr* e, expr* d, expr* cond) { - expr_ref result(d, m()); - expr_ref _cond(cond, m()); - expr* c, * a, * b; - if (m().is_false(cond)) - result = re().mk_empty(d->get_sort()); - else if (re().is_empty(d) || m().is_true(cond)) - result = d; - else if (m().is_ite(d, c, a, b)) { - expr_ref path_and_c(simplify_path(e, m().mk_and(cond, c)), m()); - expr_ref path_and_notc(simplify_path(e, m().mk_and(cond, m().mk_not(c))), m()); - result = re().mk_ite_simplify(c, mk_antimirov_deriv_restrict(e, a, path_and_c), - mk_antimirov_deriv_restrict(e, b, path_and_notc)); - } - else if (re().is_union(d, a, b)) { - expr_ref a1(mk_antimirov_deriv_restrict(e, a, cond), m()); - expr_ref b1(mk_antimirov_deriv_restrict(e, b, cond), m()); - result = mk_antimirov_deriv_union(a1, b1); - } - return result; -} expr_ref seq_rewriter::mk_regex_union_normalize(expr* r1, expr* r2) { expr_ref _r1(r1, m()), _r2(r2, m()); + expr *a1, *b1, *a2, *b2; SASSERT(m_util.is_re(r1)); SASSERT(m_util.is_re(r2)); expr_ref result(m()); std::function test = [&](expr* t, expr*& a, expr*& b) { return re().is_union(t, a, b); }; std::function compose = [&](expr* r1, expr* r2) { return (is_subset(r1, r2) ? r2 : (is_subset(r2, r1) ? r1 : re().mk_union(r1, r2))); }; + std::function is_complement = [&](expr *a, expr *b) { + expr *s; + if (re().is_complement(a, s) && s == b) + return true; + if (re().is_complement(b, s) && s == a) + return true; + return false; + }; if (r1 == r2 || re().is_empty(r2) || re().is_full_seq(r1)) result = r1; else if (re().is_empty(r1) || re().is_full_seq(r2)) @@ -3345,8 +2894,28 @@ expr_ref seq_rewriter::mk_regex_union_normalize(expr* r1, expr* r2) { result = r1; else if (re().is_dot_plus(r2) && re().get_info(r1).min_length > 0) result = r2; - else - result = merge_regex_sets(r1, r2, re().mk_full_seq(r1->get_sort()), test, compose); + // (R1 \ R2) U (R2 \ R1) = R1 xor R2 + else if (false && re().is_intersection(r1, a1, a2) && re().is_intersection(r2, b1, b2) && + is_complement(a1, b2) && is_complement(a2, b1)) { + result = re().mk_xor(a1, re().mk_complement(a2)); + } + else if (false && re().is_intersection(r1, a1, a2) && re().is_intersection(r2, b1, b2) && + is_complement(a1, b1) && is_complement(a2, b2)) { + result = re().mk_xor(a1, re().mk_complement(a2)); + } + else { + // Range ∪ Range: [a,b] ∪ [c,d] = [min(a,c), max(b,d)] when overlapping or adjacent + unsigned lo1_v = 0, hi1_v = 0, lo2_v = 0, hi2_v = 0; + if (re().is_range(r1, lo1_v, hi1_v) && re().is_range(r2, lo2_v, hi2_v) && + lo2_v <= hi1_v + 1 && lo1_v <= hi2_v + 1) { + unsigned new_lo = std::min(lo1_v, lo2_v); + unsigned new_hi = std::max(hi1_v, hi2_v); + result = re().mk_range(r1->get_sort(), new_lo, new_hi); + } + else + result = merge_regex_sets(r1, r2, re().mk_full_seq(r1->get_sort()), test, compose); + } + return result; } @@ -3375,8 +2944,17 @@ expr_ref seq_rewriter::mk_regex_inter_normalize(expr* r1, expr* r2) { result = r2; else if (re().is_dot_plus(r2) && re().get_info(r1).min_length > 0) result = r1; - else - result = merge_regex_sets(r1, r2, re().mk_empty(r1->get_sort()), test, compose); + else { + // Range ∩ Range: [a,b] ∩ [c,d] = [max(a,c), min(b,d)] or empty + unsigned lo1_v = 0, hi1_v = 0, lo2_v = 0, hi2_v = 0; + if (re().is_range(r1, lo1_v, hi1_v) && re().is_range(r2, lo2_v, hi2_v)) { + unsigned new_lo = std::max(lo1_v, lo2_v); + unsigned new_hi = std::min(hi1_v, hi2_v); + result = re().mk_range(r1->get_sort(), new_lo, new_hi); + } + else + result = merge_regex_sets(r1, r2, re().mk_empty(r1->get_sort()), test, compose); + } return result; } @@ -3483,155 +3061,6 @@ expr_ref seq_rewriter::merge_regex_sets(expr* r1, expr* r2, expr* unit, } } -expr_ref seq_rewriter::mk_regex_reverse(expr* r) { - expr* r1 = nullptr, * r2 = nullptr, * c = nullptr; - unsigned lo = 0, hi = 0; - expr_ref result(m()); - if (re().is_empty(r) || re().is_range(r) || re().is_epsilon(r) || re().is_full_seq(r) || - re().is_full_char(r) || re().is_dot_plus(r) || re().is_of_pred(r)) - result = r; - else if (re().is_to_re(r)) - result = re().mk_reverse(r); - else if (re().is_reverse(r, r1)) - result = r1; - else if (re().is_concat(r, r1, r2)) - result = mk_regex_concat(mk_regex_reverse(r2), mk_regex_reverse(r1)); - else if (m().is_ite(r, c, r1, r2)) - result = m().mk_ite(c, mk_regex_reverse(r1), mk_regex_reverse(r2)); - else if (re().is_union(r, r1, r2)) { - // enforce deterministic evaluation order - auto a1 = mk_regex_reverse(r1); - auto b1 = mk_regex_reverse(r2); - result = re().mk_union(a1, b1); - } - else if (re().is_intersection(r, r1, r2)) { - auto a1 = mk_regex_reverse(r1); - auto b1 = mk_regex_reverse(r2); - result = re().mk_inter(a1, b1); - } - else if (re().is_diff(r, r1, r2)) { - auto a1 = mk_regex_reverse(r1); - auto b1 = mk_regex_reverse(r2); - result = re().mk_diff(a1, b1); - } - else if (re().is_xor(r, r1, r2)) { - auto a1 = mk_regex_reverse(r1); - auto b1 = mk_regex_reverse(r2); - result = re().mk_xor(a1, b1); - } - else if (re().is_star(r, r1)) - result = re().mk_star(mk_regex_reverse(r1)); - else if (re().is_plus(r, r1)) - result = re().mk_plus(mk_regex_reverse(r1)); - else if (re().is_loop(r, r1, lo)) - result = re().mk_loop(mk_regex_reverse(r1), lo); - else if (re().is_loop(r, r1, lo, hi)) - result = re().mk_loop_proper(mk_regex_reverse(r1), lo, hi); - else if (re().is_opt(r, r1)) - result = re().mk_opt(mk_regex_reverse(r1)); - else if (re().is_complement(r, r1)) - result = re().mk_complement(mk_regex_reverse(r1)); - else - //stuck cases: such as r being a regex variable - //observe that re().mk_reverse(to_re(s)) is not a stuck case - result = re().mk_reverse(r); - return result; -} - -expr_ref seq_rewriter::mk_regex_concat(expr* r, expr* s) { - sort* seq_sort = nullptr, * ele_sort = nullptr; - VERIFY(m_util.is_re(r, seq_sort)); - VERIFY(u().is_seq(seq_sort, ele_sort)); - SASSERT(r->get_sort() == s->get_sort()); - expr_ref result(m()); - expr* r1, * r2; - if (re().is_epsilon(r) || re().is_empty(s)) - result = s; - else if (re().is_epsilon(s) || re().is_empty(r)) - result = r; - else if (re().is_full_seq(r) && re().is_full_seq(s)) - result = r; - else if (re().is_full_char(r) && re().is_full_seq(s)) - // ..* = .+ - result = re().mk_plus(re().mk_full_char(ele_sort)); - else if (re().is_full_seq(r) && re().is_full_char(s)) - // .*. = .+ - result = re().mk_plus(re().mk_full_char(ele_sort)); - else if (re().is_concat(r, r1, r2)) - // create the resulting concatenation in right-associative form except for the following case - // TODO: maintain the following invariant for A ++ B{m,n} + C - // concat(concat(A, B{m,n}), C) (if A != () and C != ()) - // concat(B{m,n}, C) (if A == () and C != ()) - // where A, B, C are regexes - // Using & below for Intersection and | for Union - // In other words, do not make A ++ B{m,n} into right-assoc form, but keep B{m,n} at the top - // This will help to identify this situation in the merge routine: - // concat(concat(A, B{0,m}), C) | concat(concat(A, B{0,n}), C) - // simplifies to - // concat(concat(A, B{0,max(m,n)}), C) - // analogously: - // concat(concat(A, B{0,m}), C) & concat(concat(A, B{0,n}), C) - // simplifies to - // concat(concat(A, B{0,min(m,n)}), C) - result = mk_regex_concat(r1, mk_regex_concat(r2, s)); - else { - result = re().mk_concat(r, s); - } - return result; -} - -expr_ref seq_rewriter::mk_in_antimirov(expr* s, expr* d){ - expr_ref result(mk_in_antimirov_rec(s, d), m()); - return result; -} - -expr_ref seq_rewriter::mk_in_antimirov_rec(expr* s, expr* d) { - expr* c, * d1, * d2; - expr_ref result(m()); - if (re().is_full_seq(d) || (str().min_length(s) > 0 && re().is_dot_plus(d))) - // s in .* <==> true, also: s in .+ <==> true when |s|>0 - result = m().mk_true(); - else if (re().is_empty(d) || (str().min_length(s) > 0 && re().is_epsilon(d))) - // s in [] <==> false, also: s in () <==> false when |s|>0 - result = m().mk_false(); - else if (m().is_ite(d, c, d1, d2)) - result = re().mk_ite_simplify(c, mk_in_antimirov_rec(s, d1), mk_in_antimirov_rec(s, d2)); - else if (re().is_union(d, d1, d2)) - m_br.mk_or(mk_in_antimirov_rec(s, d1), mk_in_antimirov_rec(s, d2), result); - else - result = re().mk_in_re(s, d); - return result; -} - -/* -* calls elim_condition -*/ -expr_ref seq_rewriter::simplify_path(expr* elem, expr* path) { - expr_ref result(path, m()); - elim_condition(elem, result); - return result; -} - - -expr_ref seq_rewriter::mk_der_antimirov_union(expr* r1, expr* r2) { - return mk_der_op(_OP_RE_ANTIMIROV_UNION, r1, r2); -} - -expr_ref seq_rewriter::mk_der_union(expr* r1, expr* r2) { - return mk_der_op(OP_RE_UNION, r1, r2); -} - -expr_ref seq_rewriter::mk_der_inter(expr* r1, expr* r2) { - return mk_der_op(OP_RE_INTERSECT, r1, r2); -} - -expr_ref seq_rewriter::mk_der_xor(expr* r1, expr* r2) { - return mk_der_op(OP_RE_XOR, r1, r2); -} - -expr_ref seq_rewriter::mk_der_concat(expr* r1, expr* r2) { - return mk_der_op(OP_RE_CONCAT, r1, r2); -} /* Utility functions to decide char <, ==, !=, and <=. @@ -3654,565 +3083,6 @@ bool seq_rewriter::le_char(expr* ch1, expr* ch2) { return eq_char(ch1, ch2) || lt_char(ch1, ch2); } -/* - Utility function to decide if a simple predicate (ones that appear - as the conditions in if-then-else expressions in derivatives) - implies another. - - Return true if we deduce that a implies b, false if unknown. - - Current cases handled: - - a and b are char <= constraints, or negations of char <= constraints -*/ -bool seq_rewriter::pred_implies(expr* a, expr* b) { - STRACE(seq_verbose, tout << "pred_implies: " - << "," << mk_pp(a, m()) - << "," << mk_pp(b, m()) << std::endl;); - expr *cha1 = nullptr, *cha2 = nullptr, *nota = nullptr, - *chb1 = nullptr, *chb2 = nullptr, *notb = nullptr; - if (m().is_not(a, nota) && - m().is_not(b, notb)) { - return pred_implies(notb, nota); - } - else if (u().is_char_le(a, cha1, cha2) && - u().is_char_le(b, chb1, chb2)) { - return le_char(chb1, cha1) && le_char(cha2, chb2); - } - else if (u().is_char_le(a, cha1, cha2) && - m().is_not(b, notb) && - u().is_char_le(notb, chb1, chb2)) { - return (le_char(chb2, cha1) && lt_char(cha2, chb1)) || - (lt_char(chb2, cha1) && le_char(cha2, chb1)); - } - else if (u().is_char_le(b, chb1, chb2) && - m().is_not(a, nota) && - u().is_char_le(nota, cha1, cha2)) { - return le_char(chb1, cha2) && le_char(cha1, chb2); - } - return false; -} - -/* - Utility function to decide if two BDDs (nested if-then-else terms) - have exactly the same structure and conditions. -*/ -bool seq_rewriter::ite_bdds_compatible(expr* a, expr* b) { - expr* ca = nullptr, *a1 = nullptr, *a2 = nullptr; - expr* cb = nullptr, *b1 = nullptr, *b2 = nullptr; - if (m().is_ite(a, ca, a1, a2) && m().is_ite(b, cb, b1, b2)) { - return (ca == cb) && ite_bdds_compatible(a1, b1) - && ite_bdds_compatible(a2, b2); - } - else if (m().is_ite(a) || m().is_ite(b)) { - return false; - } - else { - return true; - } -} - -/* - Apply a binary operation, preserving normal form on derivative expressions. - - Preconditions: - - k is one of the following binary op codes on REs: - OP_RE_INTERSECT - OP_RE_UNION - OP_RE_CONCAT - _OP_RE_antimirov_UNION - - a and b are in normal form (check_deriv_normal_form) - - Postcondition: - - result is in normal form (check_deriv_normal_form) -*/ -expr_ref seq_rewriter::mk_der_op_rec(decl_kind k, expr* a, expr* b) { - STRACE(seq_verbose, tout << "mk_der_op_rec: " << k - << "," << mk_pp(a, m()) - << "," << mk_pp(b, m()) << std::endl;); - expr* ca = nullptr, *a1 = nullptr, *a2 = nullptr; - expr* cb = nullptr, *b1 = nullptr, *b2 = nullptr; - expr_ref result(m()); - - // Simplify if-then-elses whenever possible - auto mk_ite = [&](expr* c, expr* a, expr* b) { - return (a == b) ? a : m().mk_ite(c, a, b); - }; - // Use character code to order conditions - auto get_id = [&](expr* e) { - expr *ch1 = nullptr, *ch2 = nullptr; - unsigned ch; - if (u().is_char_le(e, ch1, ch2) && u().is_const_char(ch2, ch)) - return ch; - // Fallback: use expression ID (but use same ID for negation) - m().is_not(e, e); - return e->get_id(); - }; - - // Choose when to lift a union to the top level, by converting - // it to an antimirov union - // This implements a restricted form of antimirov derivatives - if (k == OP_RE_UNION) { - if (re().is_antimirov_union(a) || re().is_antimirov_union(b)) { - k = _OP_RE_ANTIMIROV_UNION; - } - #if 0 - // Disabled: eager antimirov lifting unless BDDs are compatible - // Note: the check for BDD compatibility could be made more - // sophisticated: in an antimirov union of n terms, we really - // want to check if any pair of them is compatible. - else if (m().is_ite(a) && m().is_ite(b) && - !ite_bdds_compatible(a, b)) { - k = _OP_RE_ANTIMIROV_UNION; - } - #endif - } - if (k == _OP_RE_ANTIMIROV_UNION) { - result = re().mk_antimirov_union(a, b); - return result; - } - if (re().is_antimirov_union(a, a1, a2)) { - expr_ref r1(m()), r2(m()); - r1 = mk_der_op(k, a1, b); - r2 = mk_der_op(k, a2, b); - result = re().mk_antimirov_union(r1, r2); - return result; - } - if (re().is_antimirov_union(b, b1, b2)) { - expr_ref r1(m()), r2(m()); - r1 = mk_der_op(k, a, b1); - r2 = mk_der_op(k, a, b2); - result = re().mk_antimirov_union(r1, r2); - return result; - } - - // Remaining non-union case: combine two if-then-else BDDs - // (underneath top-level antimirov unions) - if (m().is_ite(a, ca, a1, a2)) { - expr_ref r1(m()), r2(m()); - expr_ref notca(m().mk_not(ca), m()); - if (m().is_ite(b, cb, b1, b2)) { - // --- Core logic for combining two BDDs - expr_ref notcb(m().mk_not(cb), m()); - if (ca == cb) { - r1 = mk_der_op(k, a1, b1); - r2 = mk_der_op(k, a2, b2); - result = mk_ite(ca, r1, r2); - return result; - } - // Order with higher IDs on the outside - bool is_symmetric = k == OP_RE_UNION || k == OP_RE_INTERSECT || k == OP_RE_XOR; - if (is_symmetric && get_id(ca) < get_id(cb)) { - std::swap(a, b); - std::swap(ca, cb); - std::swap(notca, notcb); - std::swap(a1, b1); - std::swap(a2, b2); - } - // Simplify if there is a relationship between ca and cb - if (pred_implies(ca, cb)) { - r1 = mk_der_op(k, a1, b1); - } - else if (pred_implies(ca, notcb)) { - r1 = mk_der_op(k, a1, b2); - } - if (pred_implies(notca, cb)) { - r2 = mk_der_op(k, a2, b1); - } - else if (pred_implies(notca, notcb)) { - r2 = mk_der_op(k, a2, b2); - } - // --- End core logic - } - if (!r1) r1 = mk_der_op(k, a1, b); - if (!r2) r2 = mk_der_op(k, a2, b); - result = mk_ite(ca, r1, r2); - return result; - } - if (m().is_ite(b, cb, b1, b2)) { - expr_ref r1 = mk_der_op(k, a, b1); - expr_ref r2 = mk_der_op(k, a, b2); - result = mk_ite(cb, r1, r2); - return result; - } - switch (k) { - case OP_RE_INTERSECT: - if (BR_FAILED == mk_re_inter(a, b, result)) - result = re().mk_inter(a, b); - break; - case OP_RE_UNION: - if (BR_FAILED == mk_re_union(a, b, result)) - result = re().mk_union(a, b); - break; - case OP_RE_CONCAT: - if (BR_FAILED == mk_re_concat(a, b, result)) - result = re().mk_concat(a, b); - break; - case OP_RE_XOR: - if (BR_FAILED == mk_re_xor(a, b, result)) - result = re().mk_xor(a, b); - break; - default: - UNREACHABLE(); - break; - } - - return result; -} - -expr_ref seq_rewriter::mk_der_op(decl_kind k, expr* a, expr* b) { - expr_ref _a(a, m()), _b(b, m()); - expr_ref result(m()); - - // Pre-simplification assumes that none of the - // transformations hide ite sub-terms, - // Rewriting that changes associativity of - // operators may hide ite sub-terms. - - switch (k) { - case OP_RE_INTERSECT: - if (BR_FAILED != mk_re_inter0(a, b, result)) - return result; - break; - case OP_RE_UNION: - if (BR_FAILED != mk_re_union0(a, b, result)) - return result; - break; - case OP_RE_CONCAT: - if (BR_FAILED != mk_re_concat(a, b, result)) - return result; - break; - case OP_RE_XOR: - if (BR_FAILED != mk_re_xor0(a, b, result)) - return result; - break; - default: - break; - } - result = m_op_cache.find(k, a, b, nullptr); - if (!result) { - result = mk_der_op_rec(k, a, b); - m_op_cache.insert(k, a, b, nullptr, result); - } - CASSERT("seq_regex", check_deriv_normal_form(result)); - return result; -} - -expr_ref seq_rewriter::mk_der_compl(expr* r) { - STRACE(seq_verbose, tout << "mk_der_compl: " << mk_pp(r, m()) - << std::endl;); - expr_ref result(m_op_cache.find(OP_RE_COMPLEMENT, r, nullptr, nullptr), m()); - if (!result) { - expr* c = nullptr, * r1 = nullptr, * r2 = nullptr; - if (re().is_antimirov_union(r, r1, r2)) { - // Convert union to intersection - // Result: antimirov union at top level is lost, pushed inside ITEs - expr_ref res1(m()), res2(m()); - res1 = mk_der_compl(r1); - res2 = mk_der_compl(r2); - result = mk_der_inter(res1, res2); - } - else if (m().is_ite(r, c, r1, r2)) { - result = m().mk_ite(c, mk_der_compl(r1), mk_der_compl(r2)); - } - else if (BR_FAILED == mk_re_complement(r, result)) - result = re().mk_complement(r); - m_op_cache.insert(OP_RE_COMPLEMENT, r, nullptr, nullptr, result); - } - CASSERT("seq_regex", check_deriv_normal_form(result)); - return result; -} - -/* - Make an re_predicate with an arbitrary condition cond, enforcing - derivative normal form on how conditions are written. - - Tries to rewrite everything to (ele <= x) constraints: - (ele = a) => ite(ele <= a-1, none, ite(ele <= a, epsilon, none)) - (a = ele) => " - (a <= ele) => ite(ele <= a-1, none, epsilon) - (not p) => mk_der_compl(...) - (p and q) => mk_der_inter(...) - (p or q) => mk_der_union(...) - - Postcondition: result is in BDD form -*/ -expr_ref seq_rewriter::mk_der_cond(expr* cond, expr* ele, sort* seq_sort) { - STRACE(seq_verbose, tout << "mk_der_cond: " - << mk_pp(cond, m()) << ", " << mk_pp(ele, m()) << std::endl;); - sort *ele_sort = nullptr; - VERIFY(u().is_seq(seq_sort, ele_sort)); - SASSERT(ele_sort == ele->get_sort()); - expr *c1 = nullptr, *c2 = nullptr, *ch1 = nullptr, *ch2 = nullptr; - unsigned ch = 0; - expr_ref result(m()), r1(m()), r2(m()); - if (m().is_eq(cond, ch1, ch2) && u().is_char(ch1)) { - r1 = u().mk_le(ch1, ch2); - r1 = mk_der_cond(r1, ele, seq_sort); - r2 = u().mk_le(ch2, ch1); - r2 = mk_der_cond(r2, ele, seq_sort); - result = mk_der_inter(r1, r2); - } - else if (u().is_char_le(cond, ch1, ch2) && - u().is_const_char(ch1, ch) && (ch2 == ele)) { - if (ch > 0) { - result = u().mk_char(ch - 1); - result = u().mk_le(ele, result); - result = re_predicate(result, seq_sort); - result = mk_der_compl(result); - } - else { - result = m().mk_true(); - result = re_predicate(result, seq_sort); - } - } - else if (m().is_not(cond, c1)) { - result = mk_der_cond(c1, ele, seq_sort); - result = mk_der_compl(result); - } - else if (m().is_and(cond, c1, c2)) { - r1 = mk_der_cond(c1, ele, seq_sort); - r2 = mk_der_cond(c2, ele, seq_sort); - result = mk_der_inter(r1, r2); - } - else if (m().is_or(cond, c1, c2)) { - r1 = mk_der_cond(c1, ele, seq_sort); - r2 = mk_der_cond(c2, ele, seq_sort); - result = mk_der_union(r1, r2); - } - else { - result = re_predicate(cond, seq_sort); - } - STRACE(seq_verbose, tout << "mk_der_cond result: " - << mk_pp(result, m()) << std::endl;); - CASSERT("seq_regex", check_deriv_normal_form(result)); - return result; -} - -/* - Classical Brzozowski derivative used by the regex_bisim equivalence - procedure. Unlike `mk_antimirov_deriv`, this variant never creates - _OP_RE_ANTIMIROV_UNION nodes — it stays in a classical (single regex - tree) form. The bisimulation algorithm relies on this so that each - leaf of D(p XOR q) is a coherent XOR pair (D_v p) XOR (D_v q). -*/ -expr_ref seq_rewriter::mk_derivative_rec(expr* ele, expr* r) { - expr_ref result(m()); - sort* seq_sort = nullptr, *ele_sort = nullptr; - VERIFY(m_util.is_re(r, seq_sort)); - VERIFY(m_util.is_seq(seq_sort, ele_sort)); - SASSERT(ele_sort == ele->get_sort()); - expr* r1 = nullptr, *r2 = nullptr, *p = nullptr; - auto mk_empty = [&]() { return expr_ref(re().mk_empty(r->get_sort()), m()); }; - unsigned lo = 0, hi = 0; - if (re().is_concat(r, r1, r2)) { - expr_ref is_n = is_nullable(r1); - expr_ref dr1 = mk_derivative_rec(ele, r1); - result = mk_der_concat(dr1, r2); - if (m().is_false(is_n)) { - return result; - } - expr_ref dr2 = mk_derivative_rec(ele, r2); - is_n = re_predicate(is_n, seq_sort); - if (re().is_empty(dr2)) { - //do not concatenate [], it is a deade-end - return result; - } - else { - // Classical Brzozowski union: keep the derivative tree free of - // antimirov-union nodes so the bisimulation procedure sees a - // single regex tree whose leaves are XOR pairs. - return mk_der_union(result, mk_der_concat(is_n, dr2)); - } - } - else if (re().is_star(r, r1)) { - return mk_der_concat(mk_derivative_rec(ele, r1), r); - } - else if (re().is_plus(r, r1)) { - expr_ref star(re().mk_star(r1), m()); - return mk_derivative_rec(ele, star); - } - else if (re().is_union(r, r1, r2)) { - return mk_der_union(mk_derivative_rec(ele, r1), mk_derivative_rec(ele, r2)); - } - else if (re().is_intersection(r, r1, r2)) { - return mk_der_inter(mk_derivative_rec(ele, r1), mk_derivative_rec(ele, r2)); - } - else if (re().is_diff(r, r1, r2)) { - return mk_der_inter(mk_derivative_rec(ele, r1), mk_der_compl(mk_derivative_rec(ele, r2))); - } - else if (re().is_xor(r, r1, r2)) { - return mk_der_xor(mk_derivative_rec(ele, r1), mk_derivative_rec(ele, r2)); - } - else if (m().is_ite(r, p, r1, r2)) { - // there is no BDD normalization here - result = m().mk_ite(p, mk_derivative_rec(ele, r1), mk_derivative_rec(ele, r2)); - return result; - } - else if (re().is_opt(r, r1)) { - return mk_derivative_rec(ele, r1); - } - else if (re().is_complement(r, r1)) { - return mk_der_compl(mk_derivative_rec(ele, r1)); - } - else if (re().is_loop(r, r1, lo)) { - if (lo > 0) { - lo--; - } - result = mk_derivative_rec(ele, r1); - //do not concatenate with [] (emptyset) - if (re().is_empty(result)) { - return result; - } - else { - //do not create loop r1{0,}, instead create r1* - return mk_der_concat(result, (lo == 0 ? re().mk_star(r1) : re().mk_loop(r1, lo))); - } - } - else if (re().is_loop(r, r1, lo, hi)) { - if (hi == 0) { - return mk_empty(); - } - hi--; - if (lo > 0) { - lo--; - } - result = mk_derivative_rec(ele, r1); - //do not concatenate with [] (emptyset) or handle the rest of the loop if no more iterations remain - if (re().is_empty(result) || hi == 0) { - return result; - } - else { - return mk_der_concat(result, re().mk_loop_proper(r1, lo, hi)); - } - } - else if (re().is_full_seq(r) || - re().is_empty(r)) { - return expr_ref(r, m()); - } - else if (re().is_to_re(r, r1)) { - // r1 is a string here (not a regexp) - expr_ref hd(m()), tl(m()); - if (get_head_tail(r1, hd, tl)) { - // head must be equal; if so, derivative is tail - // Use mk_der_cond to normalize - STRACE(seq_verbose, tout << "deriv to_re" << std::endl;); - result = m().mk_eq(ele, hd); - result = mk_der_cond(result, ele, seq_sort); - expr_ref r1(re().mk_to_re(tl), m()); - result = mk_der_concat(result, r1); - return result; - } - else if (str().is_empty(r1)) { - //observe: str().is_empty(r1) checks that r = () = epsilon - //while mk_empty() = [], because deriv(epsilon) = [] = nothing - return mk_empty(); - } - else if (str().is_itos(r1)) { - // - // here r1 = (str.from_int r2) and r2 is non-ground - // or else the expression would have been simplified earlier - // so r1 must be nonempty and must consists of decimal digits - // '0' <= elem <= '9' - // if ((isdigit ele) and (ele = (hd r1))) then (to_re (tl r1)) else [] - // - hd = mk_seq_first(r1); - // isolate nested conjunction for deterministic evaluation - auto a0 = u().mk_le(m_util.mk_char('0'), ele); - auto a1 = u().mk_le(ele, m_util.mk_char('9')); - auto a2 = m().mk_not(m().mk_eq(r1, str().mk_empty(seq_sort))); - auto a3 = m().mk_eq(hd, ele); - auto inner = m().mk_and(a2, a3); - m_br.mk_and(a0, a1, inner, result); - tl = re().mk_to_re(mk_seq_rest(r1)); - return re_and(result, tl); - } - else { - // recall: [] denotes the empty language (nothing) regex, () denotes epsilon or empty sequence - // construct the term (if (r1 != () and (ele = (first r1)) then (to_re (rest r1)) else [])) - hd = mk_seq_first(r1); - m_br.mk_and(m().mk_not(m().mk_eq(r1, str().mk_empty(seq_sort))), m().mk_eq(hd, ele), result); - tl = re().mk_to_re(mk_seq_rest(r1)); - return re_and(result, tl); - } - } - else if (re().is_reverse(r, r1)) { - if (re().is_to_re(r1, r2)) { - // First try to extract hd and tl such that r = hd ++ tl and |tl|=1 - expr_ref hd(m()), tl(m()); - if (get_head_tail_reversed(r2, hd, tl)) { - // Use mk_der_cond to normalize - STRACE(seq_verbose, tout << "deriv reverse to_re" << std::endl;); - result = m().mk_eq(ele, tl); - result = mk_der_cond(result, ele, seq_sort); - result = mk_der_concat(result, re().mk_reverse(re().mk_to_re(hd))); - return result; - } - else if (str().is_empty(r2)) { - return mk_empty(); - } - else { - // construct the term (if (r2 != () and (ele = (last r2)) then reverse(to_re (butlast r2)) else [])) - // hd = first of reverse(r2) i.e. last of r2 - // tl = rest of reverse(r2) i.e. butlast of r2 - //hd = str().mk_nth_i(r2, m_autil.mk_sub(str().mk_length(r2), one())); - hd = mk_seq_last(r2); - // factor nested constructor calls to enforce deterministic argument evaluation order - auto a_non_empty = m().mk_not(m().mk_eq(r2, str().mk_empty(seq_sort))); - auto a_eq = m().mk_eq(hd, ele); - m_br.mk_and(a_non_empty, a_eq, result); - tl = re().mk_to_re(mk_seq_butlast(r2)); - return re_and(result, re().mk_reverse(tl)); - } - } - } - else if (re().is_range(r, r1, r2)) { - // r1, r2 are sequences. - zstring s1, s2; - if (str().is_string(r1, s1) && str().is_string(r2, s2)) { - if (s1.length() == 1 && s2.length() == 1) { - expr_ref ch1(m_util.mk_char(s1[0]), m()); - expr_ref ch2(m_util.mk_char(s2[0]), m()); - // Use mk_der_cond to normalize - STRACE(seq_verbose, tout << "deriv range zstring" << std::endl;); - expr_ref p1(u().mk_le(ch1, ele), m()); - p1 = mk_der_cond(p1, ele, seq_sort); - expr_ref p2(u().mk_le(ele, ch2), m()); - p2 = mk_der_cond(p2, ele, seq_sort); - result = mk_der_inter(p1, p2); - return result; - } - else { - return mk_empty(); - } - } - expr* e1 = nullptr, * e2 = nullptr; - if (str().is_unit(r1, e1) && str().is_unit(r2, e2)) { - SASSERT(u().is_char(e1)); - // Use mk_der_cond to normalize - STRACE(seq_verbose, tout << "deriv range str" << std::endl;); - expr_ref p1(u().mk_le(e1, ele), m()); - p1 = mk_der_cond(p1, ele, seq_sort); - expr_ref p2(u().mk_le(ele, e2), m()); - p2 = mk_der_cond(p2, ele, seq_sort); - result = mk_der_inter(p1, p2); - return result; - } - } - else if (re().is_full_char(r)) { - return expr_ref(re().mk_to_re(str().mk_empty(seq_sort)), m()); - } - else if (re().is_of_pred(r, p)) { - array_util array(m()); - expr* args[2] = { p, ele }; - result = array.mk_select(2, args); - // Use mk_der_cond to normalize - STRACE(seq_verbose, tout << "deriv of_pred" << std::endl;); - return mk_der_cond(result, ele, seq_sort); - } - // stuck cases: re.derivative, re variable, - return expr_ref(re().mk_derivative(ele, r), m()); -} - /************************************************* ***** End Derivative Code ***** *************************************************/ @@ -4438,6 +3308,23 @@ br_status seq_rewriter::mk_str_in_regexp(expr* a, expr* b, expr_ref& result) { } } + + // replace_all(x, a, b) in R where R is ground, a and b are unit-length strings + // ==> x in R[b -> {a, b}, a -> empty] + expr *ra_x = nullptr, *ra_a = nullptr, *ra_b = nullptr; + zstring sa_val, sb_val; + if (str().is_replace_all(a, ra_x, ra_a, ra_b) && ra_a == ra_b) { + result = ra_x; + return BR_DONE; + } + if (str().is_replace_all(a, ra_x, ra_a, ra_b) && str().is_string(ra_a, sa_val) && sa_val.length() == 1 && + str().is_string(ra_b, sb_val) && sb_val.length() == 1 && sa_val[0] != sb_val[0] && re().is_ground(b) && + re().get_info(b).classical) { + expr_ref new_re = re_replace_char(b, sa_val[0], sb_val[0], ra_a, ra_b); + result = re().mk_in_re(ra_x, new_re); + return BR_REWRITE_FULL; + } + expr_ref b_s(m()); if (lift_str_from_to_re(b, b_s)) { result = m_br.mk_eq_rw(a, b_s); @@ -4473,9 +3360,6 @@ br_status seq_rewriter::mk_str_in_regexp(expr* a, expr* b, expr_ref& result) { return BR_REWRITE_FULL; } - return BR_FAILED; - // disabled - #if 0 expr_ref hd(m()), tl(m()); @@ -4639,6 +3523,17 @@ br_status seq_rewriter::mk_re_concat(expr* a, expr* b, expr_ref& result) { result = b; return BR_DONE; } + // Collapse adjacent full_seq factors regardless of concat grouping: + // (R ++ Σ*) ++ Σ* → R ++ Σ* (a ends with Σ*, b is Σ*) + // Σ* ++ (Σ* ++ R) → Σ* ++ R (a is Σ*, b starts with Σ*) + if (re().is_full_seq(b) && ends_with_full_seq(a)) { + result = a; + return BR_DONE; + } + if (re().is_full_seq(a) && starts_with_full_seq(b)) { + result = b; + return BR_DONE; + } expr* u1 = nullptr, *u2 = nullptr; if (re().is_full_seq(a) && re().is_union(b, u1, u2) && (starts_with_full_seq(u1) || starts_with_full_seq(u2))) { @@ -4674,18 +3569,13 @@ br_status seq_rewriter::mk_re_concat(expr* a, expr* b, expr_ref& result) { result = re().mk_plus(b); return BR_DONE; } - expr *x = nullptr, *y = nullptr; - if (re().is_concat(a, x, y) && re().is_full_seq(y) && re().is_full_seq(b)) { - result = a; - return BR_DONE; - } expr_ref a_str(m()); expr_ref b_str(m()); if (lift_str_from_to_re(a, a_str) && lift_str_from_to_re(b, b_str)) { result = re().mk_to_re(str().mk_concat(a_str, b_str)); return BR_REWRITE2; } - expr* a1 = nullptr; + expr *a1 = nullptr, *a2 = nullptr; expr* b1 = nullptr; if (re().is_to_re(a, a1) && re().is_to_re(b, b1)) { result = re().mk_to_re(str().mk_concat(a1, b1)); @@ -4706,6 +3596,7 @@ br_status seq_rewriter::mk_re_concat(expr* a, expr* b, expr_ref& result) { } unsigned lo1, hi1, lo2, hi2; + if (re().is_loop(a, a1, lo1, hi1) && lo1 <= hi1 && re().is_loop(b, b1, lo2, hi2) && lo2 <= hi2 && a1 == b1) { result = re().mk_loop_proper(a1, lo1 + lo2, hi1 + hi2); return BR_DONE; @@ -4737,9 +3628,68 @@ br_status seq_rewriter::mk_re_concat(expr* a, expr* b, expr_ref& result) { } std::swap(a, b); } + // Hoist ite out of concat: concat(ite(c, r1, r2), b) → ite(c, concat(r1, b), concat(r2, b)) + expr* c = nullptr; + if (m().is_ite(a, c, a1, b1)) { + result = m().mk_ite(c, re().mk_concat(a1, b), re().mk_concat(b1, b)); + return BR_REWRITE3; + } + if (m().is_ite(b, c, a1, b1)) { + result = m().mk_ite(c, re().mk_concat(a, a1), re().mk_concat(a, b1)); + return BR_REWRITE3; + } + if (re().is_concat(a, a1, a2)) { + // Maintain right-associative normal form: re().mk_concat is a raw + // constructor, so re-simplify the result to recursively reassociate + // any concat nested in a2 (and re-apply concat simplifications). + result = re().mk_concat(a1, re().mk_concat(a2, b)); + return BR_DONE; + } return BR_FAILED; } +expr_ref seq_rewriter::mk_regex_concat(expr *r, expr *s) { + sort *seq_sort = nullptr, *ele_sort = nullptr; + VERIFY(m_util.is_re(r, seq_sort)); + VERIFY(u().is_seq(seq_sort, ele_sort)); + SASSERT(r->get_sort() == s->get_sort()); + expr_ref result(m()); + expr *r1, *r2; + if (re().is_epsilon(r) || re().is_empty(s)) + result = s; + else if (re().is_epsilon(s) || re().is_empty(r)) + result = r; + else if (re().is_full_seq(r) && re().is_full_seq(s)) + result = r; + else if (re().is_full_char(r) && re().is_full_seq(s)) + // ..* = .+ + result = re().mk_plus(re().mk_full_char(r->get_sort())); + else if (re().is_full_seq(r) && re().is_full_char(s)) + // .*. = .+ + result = re().mk_plus(re().mk_full_char(r->get_sort())); + else if (re().is_concat(r, r1, r2)) + // create the resulting concatenation in right-associative form except for the following case + // TODO: maintain the following invariant for A ++ B{m,n} + C + // concat(concat(A, B{m,n}), C) (if A != () and C != ()) + // concat(B{m,n}, C) (if A == () and C != ()) + // where A, B, C are regexes + // Using & below for Intersection and | for Union + // In other words, do not make A ++ B{m,n} into right-assoc form, but keep B{m,n} at the top + // This will help to identify this situation in the merge routine: + // concat(concat(A, B{0,m}), C) | concat(concat(A, B{0,n}), C) + // simplifies to + // concat(concat(A, B{0,max(m,n)}), C) + // analogously: + // concat(concat(A, B{0,m}), C) & concat(concat(A, B{0,n}), C) + // simplifies to + // concat(concat(A, B{0,min(m,n)}), C) + result = mk_regex_concat(r1, mk_regex_concat(r2, s)); + else { + result = re().mk_concat(r, s); + } + return result; +} + bool seq_rewriter::are_complements(expr* r1, expr* r2) const { expr* r = nullptr; if (re().is_complement(r1, r) && r == r2) @@ -4756,6 +3706,32 @@ bool seq_rewriter::is_subset(expr* r1, expr* r2) const { return m_subset.is_subset(r1, r2); } +bool seq_rewriter::try_collapse_re_union(expr* a, expr* b, expr_ref& result) { + sort* seq_sort = nullptr; + if (!u().is_re(a->get_sort(), seq_sort)) + return false; + seq::range_predicate pa(u().max_char()), pb(u().max_char()); + if (!seq::regex_to_range_predicate(u(), a, pa)) + return false; + if (!seq::regex_to_range_predicate(u(), b, pb)) + return false; + result = seq::range_predicate_to_regex(u(), pa | pb, seq_sort); + return true; +} + +bool seq_rewriter::try_collapse_re_inter(expr* a, expr* b, expr_ref& result) { + sort* seq_sort = nullptr; + if (!u().is_re(a->get_sort(), seq_sort)) + return false; + seq::range_predicate pa(u().max_char()), pb(u().max_char()); + if (!seq::regex_to_range_predicate(u(), a, pa)) + return false; + if (!seq::regex_to_range_predicate(u(), b, pb)) + return false; + result = seq::range_predicate_to_regex(u(), pa & pb, seq_sort); + return true; +} + br_status seq_rewriter::mk_re_union0(expr* a, expr* b, expr_ref& result) { if (a == b) { result = a; @@ -4785,11 +3761,30 @@ br_status seq_rewriter::mk_re_union0(expr* a, expr* b, expr_ref& result) { result = b; return BR_DONE; } + // r ∪ ~r → Σ* (complement absorption) + if (are_complements(a, b)) { + result = re().mk_full_seq(a->get_sort()); + return BR_DONE; + } + // Hoist ite out of union: union(ite(c, r1, r2), b) → ite(c, union(r1, b), union(r2, b)) + expr *c = nullptr, *r1 = nullptr, *r2 = nullptr; + if (m().is_ite(a, c, r1, r2)) { + result = m().mk_ite(c, re().mk_union(r1, b), re().mk_union(r2, b)); + return BR_REWRITE3; + } + if (m().is_ite(b, c, r1, r2)) { + result = m().mk_ite(c, re().mk_union(a, r1), re().mk_union(a, r2)); + return BR_REWRITE3; + } + if (try_collapse_re_union(a, b, result)) + return BR_DONE; return BR_FAILED; } /* Creates a normalized union. */ br_status seq_rewriter::mk_re_union(expr* a, expr* b, expr_ref& result) { + if (try_collapse_re_union(a, b, result)) + return BR_DONE; result = mk_regex_union_normalize(a, b); return BR_DONE; } @@ -4827,6 +3822,12 @@ br_status seq_rewriter::mk_re_complement(expr* a, expr_ref& result) { result = re().mk_plus(re().mk_full_char(a->get_sort())); return BR_DONE; } + // Hoist ite out of complement: ~(ite(c, r1, r2)) → ite(c, ~r1, ~r2) + expr* c = nullptr; + if (m().is_ite(a, c, e1, e2)) { + result = m().mk_ite(c, re().mk_complement(e1), re().mk_complement(e2)); + return BR_REWRITE3; + } return BR_FAILED; } @@ -4852,16 +3853,43 @@ br_status seq_rewriter::mk_re_inter0(expr* a, expr* b, expr_ref& result) { result = a; return BR_DONE; } + // r ∩ ~r → ∅ (complement absorption) + if (are_complements(a, b)) { + result = re().mk_empty(a->get_sort()); + return BR_DONE; + } + // Hoist ite out of intersection: inter(ite(c, r1, r2), b) → ite(c, inter(r1, b), inter(r2, b)) + expr *c = nullptr, *r1 = nullptr, *r2 = nullptr; + if (m().is_ite(a, c, r1, r2)) { + result = m().mk_ite(c, re().mk_inter(r1, b), re().mk_inter(r2, b)); + return BR_REWRITE3; + } + if (m().is_ite(b, c, r1, r2)) { + result = m().mk_ite(c, re().mk_inter(a, r1), re().mk_inter(a, r2)); + return BR_REWRITE3; + } + if (try_collapse_re_inter(a, b, result)) + return BR_DONE; return BR_FAILED; } /* Creates a normalized intersection. */ br_status seq_rewriter::mk_re_inter(expr* a, expr* b, expr_ref& result) { + if (try_collapse_re_inter(a, b, result)) + return BR_DONE; result = mk_regex_inter_normalize(a, b); return BR_DONE; } br_status seq_rewriter::mk_re_diff(expr* a, expr* b, expr_ref& result) { + seq::range_predicate pa(u().max_char()), pb(u().max_char()); + sort* seq_sort = nullptr; + if (u().is_re(a->get_sort(), seq_sort) + && seq::regex_to_range_predicate(u(), a, pa) + && seq::regex_to_range_predicate(u(), b, pb)) { + result = seq::range_predicate_to_regex(u(), pa - pb, seq_sort); + return BR_DONE; + } result = mk_regex_inter_normalize(a, re().mk_complement(b)); return BR_REWRITE2; } @@ -5091,7 +4119,9 @@ br_status seq_rewriter::mk_re_star(expr* a, expr_ref& result) { result = re().mk_full_seq(b1->get_sort()); return BR_REWRITE2; } - + // Hoist ite out of star: (ite c r1 r2)* → ite(c, r1*, r2*) + result = m().mk_ite(c, re().mk_star(b1), re().mk_star(c1)); + return BR_REWRITE3; } return BR_FAILED; } @@ -5101,14 +4131,10 @@ br_status seq_rewriter::mk_re_star(expr* a, expr_ref& result) { */ br_status seq_rewriter::mk_re_range(expr* lo, expr* hi, expr_ref& result) { zstring slo, shi; + unsigned clo = 0, chi = 0; + expr *lo1, *hi1; unsigned len = 0; bool is_empty = false; - if (str().is_string(lo, slo) && slo.length() != 1) - is_empty = true; - if (str().is_string(hi, shi) && shi.length() != 1) - is_empty = true; - if (slo.length() == 1 && shi.length() == 1 && slo[0] > shi[0]) - is_empty = true; len = min_length(lo).second; if (len > 1) is_empty = true; @@ -5119,12 +4145,41 @@ br_status seq_rewriter::mk_re_range(expr* lo, expr* hi, expr_ref& result) { is_empty = true; if (max_length(hi) == std::make_pair(true, rational(0))) is_empty = true; + if (!is_empty) { + if (str().is_string(lo, slo) && slo.length() == 1) + clo = slo[0]; + else if (str().is_unit(lo, lo1) && m_util.is_const_char(lo1, clo)) + ; + else + is_empty = true; + } + if (!is_empty) { + if (str().is_string(hi, shi) && shi.length() == 1) + chi = shi[0]; + else if (str().is_unit(hi, hi1) && m_util.is_const_char(hi1, chi)) + ; + else + is_empty = true; + } + + // clo/chi are only meaningful once both bounds were extracted; an early + // is_empty (from the length checks) leaves them at their default 0, so the + // is_empty return must come before the singleton/ordering checks below. + if (!is_empty && clo > chi) + is_empty = true; + if (is_empty) { sort* srt = re().mk_re(lo->get_sort()); result = re().mk_empty(srt); return BR_DONE; } + // Singleton: re.range "a" "a" → str.to_re "a" + if (clo == chi) { + result = re().mk_to_re(str().mk_string(zstring(clo))); + return BR_DONE; + } + return BR_FAILED; } @@ -5382,7 +4437,7 @@ br_status seq_rewriter::reduce_re_eq(expr* l, expr* r, expr_ref& result) { * indirectly. On l_undef the rewriter falls through to the existing * axiomatisation path. */ - if (!m_in_bisim && is_ground(l) && is_ground(r)) { + if (!m_in_bisim && re().is_ground(l) && re().is_ground(r)) { flet _block(m_in_bisim, true); seq::regex_bisim bisim(*this); switch (bisim.are_equivalent(l, r)) { @@ -5435,6 +4490,25 @@ br_status seq_rewriter::mk_eq_core(expr * l, expr * r, expr_ref & result) { if (reduce_eq_empty(l, r, result)) return BR_REWRITE_FULL; + // a, b are unit-length ground strings => replace_all(x, a, b) in re.to_re(s) + { + expr *ra_x = nullptr, *ra_a = nullptr, *ra_b = nullptr; + zstring sa_val, sb_val, s_val; + expr *str_side = nullptr, *ra_side = nullptr; + if (str().is_replace_all(l)) + ra_side = l, str_side = r; + else if (str().is_replace_all(r)) + ra_side = r, str_side = l; + if (ra_side && str_side && + str().is_replace_all(ra_side, ra_x, ra_a, ra_b) && str().is_string(ra_a, sa_val) && + sa_val.length() == 1 && + str().is_string(ra_b, sb_val) && sb_val.length() == 1 && + str().is_string(str_side, s_val)) { + result = re().mk_in_re(ra_side, re().mk_to_re(str_side)); + return BR_REWRITE_FULL; + } + } + #if 0 if (reduce_arith_eq(l, r, res) || reduce_arith_eq(r, l, res)) { result = mk_and(res); @@ -6249,31 +5323,10 @@ void seq_rewriter::op_cache::cleanup() { } } -lbool seq_rewriter::some_seq_in_re(expr* r, expr_ref& result) { - sort* seq_sort = nullptr; - if (!u().is_re(r, seq_sort)) - return l_undef; - if (u().is_string(seq_sort)) { - zstring s; - lbool res = some_string_in_re(r, s); - if (res == l_true) - result = str().mk_string(s); - return res; - } - // For non-string sequences: check if the regex accepts the empty sequence. - SASSERT(seq_sort); - expr_ref is_null = is_nullable(r); - if (m().is_true(is_null)) { - result = str().mk_empty(seq_sort); - return l_true; - } - return l_undef; -} - lbool seq_rewriter::some_string_in_re(expr* r, zstring& s) { sort* rs; (void)rs; - // SASSERT(re().is_re(r, rs) && m_util.is_string(rs)); + // SASSERT(u().is_re(r, rs) && m_util.is_string(rs)); expr_mark visited; unsigned_vector str; @@ -6301,6 +5354,7 @@ lbool seq_rewriter::some_string_in_re(expr_mark& visited, expr* r, unsigned_vect str.resize(current.str_len); if (current.needs_derivation) { SASSERT(current.exclude.empty()); + // We are looking for the next character => generate derivation if (visited.is_marked(r)) continue; if (re().is_empty(r)) @@ -6308,11 +5362,11 @@ lbool seq_rewriter::some_string_in_re(expr_mark& visited, expr* r, unsigned_vect auto info = re().get_info(r); if (info.nullable == l_true) return l_true; - IF_VERBOSE(2, verbose_stream() << " derive str=" << str.size() << " " << mk_bounded_pp(r, m(), 2) << "\n"); visited.mark(r); if (re().is_union(r)) { - for (expr* arg : *to_app(r)) + for (expr* arg : *to_app(r)) { todo.push_back({ expr_ref(arg, m()), str.size(), {}, true }); + } continue; } @@ -6322,7 +5376,7 @@ lbool seq_rewriter::some_string_in_re(expr_mark& visited, expr* r, unsigned_vect buffer> exclude = std::move(current.exclude); - + expr* c, * th, * el; if (re().is_empty(r)) continue; if (re().is_union(r)) { @@ -6331,58 +5385,24 @@ lbool seq_rewriter::some_string_in_re(expr_mark& visited, expr* r, unsigned_vect } continue; } - expr_ref c(m()), th(m()), el(m()); - if (bool_rewriter(m()).decompose_ite(r, c, th, el)) { + if (m().is_ite(r, c, th, el)) { unsigned low = 0, high = zstring::unicode_max_char(); bool has_bounds = get_bounds(c, low, high); if (!re().is_empty(el)) { if (has_bounds) exclude.push_back({ low, high }); - todo.push_back({ expr_ref(el, m()), str.size(), exclude, false }); - if (has_bounds) - exclude.pop_back(); + todo.push_back({ expr_ref(el, m()), str.size(), std::move(exclude), false }); } if (has_bounds) { - - if (any_of(subterms::ground(th), [&](expr *t) { return m().is_ite(t); })) { - if (low > 0) - exclude.push_back({0, low - 1}); - if (high < zstring::unicode_max_char()) - exclude.push_back({high + 1, zstring::unicode_max_char()}); - todo.push_back({ expr_ref(th, m()), str.size(), exclude, false }); - continue; - } - + // I want this case to be processed first => push it last + // reason: current string is only pruned SASSERT(low <= high); - unsigned ch = low; - bool found = true; - while (true) { - bool advanced = false; - for (auto [l, h] : exclude) { - if (l <= ch && ch <= h) { - if (h >= high) { - found = false; - break; - } - ch = h + 1; - advanced = true; - } - } - if (!found || ch > high) - break; - if (!advanced) - break; - } - if (found && ch <= high) { - str.push_back(ch); - todo.push_back({expr_ref(th, m()), str.size(), {}, true}); - } + str.push_back(low); // ASSERT: low .. high does not intersect with exclude + todo.push_back({ expr_ref(th, m()), str.size(), {}, true }); } continue; } - - if (is_ground(r)) { // ensure selected character is not in exclude unsigned ch = 'a'; diff --git a/src/ast/rewriter/seq_rewriter.h b/src/ast/rewriter/seq_rewriter.h index 7972b8086e..d295c201e0 100644 --- a/src/ast/rewriter/seq_rewriter.h +++ b/src/ast/rewriter/seq_rewriter.h @@ -18,7 +18,9 @@ Notes: --*/ #pragma once +#include "seq_split.h" #include "ast/seq_decl_plugin.h" +#include "ast/rewriter/seq_derive.h" #include "ast/ast_pp.h" #include "ast/arith_decl_plugin.h" #include "ast/rewriter/rewriter_types.h" @@ -129,16 +131,21 @@ class seq_rewriter { void insert(decl_kind op, expr* a, expr* b, expr* c, expr* r); }; + friend class seq::derive; + seq_util m_util; seq_subset m_subset; seq_split m_split; arith_util m_autil; bool_rewriter m_br; + seq::derive m_derive; // re2automaton m_re2aut; op_cache m_op_cache; expr_ref_vector m_es, m_lhs, m_rhs; - bool m_coalesce_chars; - bool m_in_bisim { false }; + bool m_coalesce_chars = true; + bool m_in_bisim { false }; + unsigned m_re_deriv_depth { 0 }; + static const unsigned m_max_re_deriv_depth = 512; enum length_comparison { shorter_c, @@ -175,50 +182,22 @@ class seq_rewriter { //replace b in a by c into result void replace_all_subvectors(expr_ref_vector const& as, expr_ref_vector const& bs, expr* c, expr_ref_vector& result); - // Calculate derivative, memoized and enforcing a normal form - expr_ref is_nullable_rec(expr* r); - expr_ref mk_derivative_rec(expr* ele, expr* r); - expr_ref mk_der_op(decl_kind k, expr* a, expr* b); - expr_ref mk_der_op_rec(decl_kind k, expr* a, expr* b); - expr_ref mk_der_concat(expr* a, expr* b); - expr_ref mk_der_union(expr* a, expr* b); - expr_ref mk_der_inter(expr* a, expr* b); - expr_ref mk_der_xor(expr* a, expr* b); - expr_ref mk_der_compl(expr* a); - expr_ref mk_der_cond(expr* cond, expr* ele, sort* seq_sort); - expr_ref mk_der_antimirov_union(expr* r1, expr* r2); - bool ite_bdds_compatible(expr* a, expr* b); - /* if r has the form deriv(en..deriv(e1,to_re(s))..) returns 's = [e1..en]' else returns '() in r'*/ - expr_ref is_nullable_symbolic_regex(expr* r, sort* seq_sort); - #ifdef Z3DEBUG - bool check_deriv_normal_form(expr* r, int level = 3); - #endif + // For replace_all(x, a, b) in R: transform R so that + // - occurrences of b_ch are replaced by union(to_re(a_str), to_re(b_str)) + // - occurrences of a_ch are replaced by empty (replace_all never outputs a) + expr_ref re_replace_char(expr *r, unsigned a_ch, unsigned b_ch, expr *a_str, expr *b_str); - void mk_antimirov_deriv_rec(expr* e, expr* r, expr* path, expr_ref& result); - - expr_ref mk_antimirov_deriv(expr* e, expr* r, expr* path); - expr_ref mk_in_antimirov_rec(expr* s, expr* d); - expr_ref mk_in_antimirov(expr* s, expr* d); - - expr_ref mk_antimirov_deriv_intersection(expr* elem, expr* d1, expr* d2, expr* path); - expr_ref mk_antimirov_deriv_concat(expr* d, expr* r); - expr_ref mk_antimirov_deriv_negate(expr* elem, expr* d); - expr_ref mk_antimirov_deriv_union(expr* d1, expr* d2); - expr_ref mk_antimirov_deriv_restrict(expr* elem, expr* d1, expr* cond); - expr_ref mk_regex_reverse(expr* r); - expr_ref mk_regex_concat(expr* r1, expr* r2); expr_ref merge_regex_sets(expr* r1, expr* r2, expr* unit, std::function& decompose, std::function& compose); // elem is (:var 0) and path a condition that may have (:var 0) as a free variable // simplify path, e.g., (:var 0) = 'a' & (:var 0) = 'b' is simplified to false - expr_ref simplify_path(expr* elem, expr* path); + // expr_ref simplify_path(expr* elem, expr* path); bool lt_char(expr* ch1, expr* ch2); bool eq_char(expr* ch1, expr* ch2); bool neq_char(expr* ch1, expr* ch2); bool le_char(expr* ch1, expr* ch2); - bool pred_implies(expr* a, expr* b); bool are_complements(expr* r1, expr* r2) const; bool is_subset(expr* r1, expr* r2) const; @@ -264,6 +243,14 @@ class seq_rewriter { br_status mk_re_union0(expr* a, expr* b, expr_ref& result); br_status mk_re_inter0(expr* a, expr* b, expr_ref& result); br_status mk_re_complement(expr* a, expr_ref& result); + // Range-set collapse helpers: if the operands form a boolean + // combination of character-class regexes, materialize the result as a + // canonical regex over a single range_predicate. See + // ast/rewriter/seq_range_collapse.h for the recognized fragment. + // NOTE: re.complement is intentionally not in this set because it + // operates at the sequence level, not the character-class level. + bool try_collapse_re_union(expr* a, expr* b, expr_ref& result); + bool try_collapse_re_inter(expr* a, expr* b, expr_ref& result); br_status mk_re_star(expr* a, expr_ref& result); br_status mk_re_diff(expr* a, expr* b, expr_ref& result); br_status mk_re_xor(expr* a, expr* b, expr_ref& result); @@ -347,10 +334,10 @@ class seq_rewriter { lbool some_string_in_re(expr_mark& visited, expr* r, unsigned_vector& str); public: - seq_rewriter(ast_manager & m, params_ref const & p = params_ref()): - m_util(m), m_subset(m_util.re), m_split(*this), m_autil(m), m_br(m, p), // m_re2aut(m), + seq_rewriter(ast_manager & m, params_ref const & p = params_ref()) : + m_util(m), m_subset(m_util.re), m_split(*this), m_autil(m), m_br(m, p), m_derive(m, *this), // m_re2aut(m), m_op_cache(m), m_es(m), - m_lhs(m), m_rhs(m), m_coalesce_chars(true) { + m_lhs(m), m_rhs(m) { } ast_manager & m() const { return m_util.get_manager(); } family_id get_fid() const { return m_util.get_family_id(); } @@ -361,7 +348,7 @@ public: static void get_param_descrs(param_descrs & r); - bool coalesce_chars() const { return m_coalesce_chars; } + // bool coalesce_chars() const { return m_coalesce_chars; } br_status mk_app_core(func_decl * f, unsigned num_args, expr * const * args, expr_ref & result); br_status mk_eq_core(expr * lhs, expr * rhs, expr_ref & result); @@ -377,6 +364,34 @@ public: return result; } + expr_ref mk_xor0(expr *a, expr *b) { + expr_ref result(m()); + if (mk_re_xor0(a, b, result) == BR_FAILED) + result = re().mk_xor(a, b); + return result; + } + + expr_ref mk_union(expr *a, expr *b) { + expr_ref result(m()); + if (mk_re_union(a, b, result) == BR_FAILED) + result = re().mk_union(a, b); + return result; + } + + expr_ref mk_inter(expr *a, expr *b) { + expr_ref result(m()); + if (mk_re_inter(a, b, result) == BR_FAILED) + result = re().mk_inter(a, b); + return result; + } + + expr_ref mk_complement(expr *a) { + expr_ref result(m()); + if (mk_re_complement(a, result) == BR_FAILED) + result = re().mk_complement(a); + return result; + } + /* * makes concat and simplifies */ @@ -460,11 +475,35 @@ public: variable v0 = (:var 0). Unlike `mk_derivative` this entry point keeps the symbolic derivative as a single transition regex (TRegex): boolean operators are pushed into the ITE leaves rather than lifted to the top - via _OP_RE_ANTIMIROV_UNION. Used by the regex_bisim equivalence + as a union. Used by the regex_bisim equivalence procedure which relies on each leaf of D(p XOR q) being a coherent XOR pair (D_v p) XOR (D_v q). */ - expr_ref mk_brz_derivative(expr* r); + expr_ref mk_brz_derivative(expr *r) { + return mk_derivative(r); + } + + /* + Enumerate the cofactors (min-terms) of a transition regex r taken with + respect to ele. Produces (path_condition, leaf_regex) pairs for every + feasible path through the ITE-tree, pruning infeasible character ranges. + Delegates to the derivative engine so the same path/interval context used + while hoisting ITEs is reused for the leaf simplification. + */ + void get_cofactors(expr* ele, expr* r, expr_ref_pair_vector& result) { + m_derive.get_cofactors(ele, r, result); + } + + /* + Compute the symbolic derivative of r and enumerate its reachable leaves + in fully ITE-hoisted normal form: a list of (path_condition, target) + pairs where every target is free of (:var 0) (so nullability is always + decidable) and unions are kept intact as single states. Used by + regex_bisim, which consumes the targets and ignores the path conditions. + */ + void brz_derivative_cofactors(expr* r, expr_ref_pair_vector& result) { + m_derive.derivative_cofactors(r, result); + } // heuristic elimination of element from condition that comes form a derivative. // special case optimization for conjunctions of equalities, disequalities and ranges. @@ -475,15 +514,7 @@ public: /* Apply simplifications to the intersection to keep it normalized (r1 and r2 are not normalized)*/ expr_ref mk_regex_inter_normalize(expr* r1, expr* r2); - /* - * Extract some sequence that is a member of r. - * result is set to a concrete sequence expression if l_true is returned. - * For string-typed regexes, delegates to some_string_in_re. - * For other sequence types, checks nullability and returns the empty - * sequence if the regex accepts it; otherwise returns l_undef. - * Returns l_false if the regex is known to be empty. - */ - lbool some_seq_in_re(expr* r, expr_ref& result); + expr_ref mk_regex_concat(expr *r1, expr *r2); /* * Extract some string that is a member of r. diff --git a/src/ast/rewriter/seq_split.cpp b/src/ast/rewriter/seq_split.cpp index 8977065fc0..fcba3a8d03 100644 --- a/src/ast/rewriter/seq_split.cpp +++ b/src/ast/rewriter/seq_split.cpp @@ -1,3 +1,4 @@ + /*++ Copyright (c) 2026 Microsoft Corporation diff --git a/src/ast/rewriter/seq_subset.cpp b/src/ast/rewriter/seq_subset.cpp index 2fc4d1f715..1af42d06d8 100644 --- a/src/ast/rewriter/seq_subset.cpp +++ b/src/ast/rewriter/seq_subset.cpp @@ -19,7 +19,7 @@ Author: bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const { while (true) { - + if (a == b) return true; if (m_re.is_empty(a)) @@ -30,7 +30,7 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const { return true; if (depth >= m_max_depth) - return false; + return false; expr* a1 = nullptr, * a2 = nullptr, * b1 = nullptr, * b2 = nullptr; unsigned la, ua, lb, ub; @@ -39,16 +39,12 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const { if (m_re.is_dot_plus(b) && m_re.get_info(a).nullable == l_false) return true; - // a ⊆ a* - if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth)) - return true; - // e ⊆ a* if (m_re.is_epsilon(a) && m_re.is_star(b, b1)) return true; - // R ⊆ R* - if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth + 1)) + // a ⊆ a*: if b = b1* and a ⊆ b1, then a ⊆ b1* + if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth)) return true; // R1* ⊆ R2* if R1 ⊆ R2 @@ -112,6 +108,12 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const { if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b1) && is_subset_rec(a, b2, depth)) return true; + // prefix absorption: P·R' ⊆ Σ*·R' for any prefix P (since P ⊆ Σ*). + // Detect that a has R' (= b2) as a concatenation suffix, where b = Σ*·R'. + // Covers contains-patterns, e.g. Σ*·a·Σ*·b·Σ* ⊆ Σ*·b·Σ*. + if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b1) && ends_with(a, b2)) + return true; + // R ⊆ R'·Σ* if R ⊆ R' if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b2) && is_subset_rec(a, b1, depth)) return true; @@ -144,3 +146,30 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const { bool seq_subset::is_subset(expr* a, expr* b) const { return is_subset_rec(a, b, 0); } + +bool seq_subset::ends_with(expr* a, expr* suf) const { + if (a == suf) + return true; + // Flatten both regexes into their sequence of concatenation factors + // (independent of left/right associativity) and test list-suffix equality. + ptr_vector af, sf; + flatten_concat(a, af); + flatten_concat(suf, sf); + if (sf.size() > af.size()) + return false; + unsigned off = af.size() - sf.size(); + for (unsigned i = 0; i < sf.size(); ++i) + if (af[off + i] != sf[i]) + return false; + return true; +} + +void seq_subset::flatten_concat(expr* a, ptr_vector& out) const { + expr* a1 = nullptr, * a2 = nullptr; + if (m_re.is_concat(a, a1, a2)) { + flatten_concat(a1, out); + flatten_concat(a2, out); + } + else + out.push_back(a); +} diff --git a/src/ast/rewriter/seq_subset.h b/src/ast/rewriter/seq_subset.h index 7329c898e1..e62333dea3 100644 --- a/src/ast/rewriter/seq_subset.h +++ b/src/ast/rewriter/seq_subset.h @@ -24,6 +24,12 @@ class seq_subset { bool is_subset_rec(expr* a, expr* b, unsigned depth) const; + // true if regex a, viewed as a flattened concatenation, has suf as a + // structural (concatenation) suffix. + bool ends_with(expr* a, expr* suf) const; + + void flatten_concat(expr* a, ptr_vector& out) const; + public: explicit seq_subset(seq_util::rex& re) : m_re(re) {} bool is_subset(expr* a, expr* b) const; diff --git a/src/ast/rewriter/term_enumeration.cpp b/src/ast/rewriter/term_enumeration.cpp new file mode 100644 index 0000000000..2397db467b --- /dev/null +++ b/src/ast/rewriter/term_enumeration.cpp @@ -0,0 +1,681 @@ +/** + * term_enumeration.cpp - Bottom-up term enumeration module for Z3 + * + * Inspired by the Probe synthesizer (Barke et al., "Just-in-Time Learning + * for Bottom-Up Enumerative Synthesis"). Adapted to use Z3's internal APIs. + * + * Key ideas: + * - Terms are enumerated bottom-up by "cost" (calculated by tree size). + * - A grammar describes which function symbols (operators) and leaves + * (constants, variables) are available for enumeration. + */ + +#include +#include +#include +#include "util/vector.h" +#include "util/scoped_ptr_vector.h" +#include "util/obj_hashtable.h" +#include "util/uint_set.h" +#include "ast/ast.h" +#include "ast/ast_ll_pp.h" +#include "ast/ast_pp.h" +#include "ast/rewriter/th_rewriter.h" +#include "ast/rewriter/term_enumeration.h" + + +namespace term_enum { + +// ============================================================================ +// grammar production rule +// ============================================================================ + +/** + * A production describes how to construct a term from child terms. + * - domain: the sort required for each child + * - range: the sort of the produced term + * - builder: given a vector of child exprs, produce the result expr + */ +struct production { + std::string name; + sort_ref range; + sort_ref_vector domain; + std::function builder; + + bool is_leaf() const { return domain.empty(); } +}; + +// ============================================================================ +// grammar +// ============================================================================ + +/** + * A grammar groups productions into leaves (arity 0) and operators (arity > 0). + */ +class grammar { +public: + grammar(ast_manager& m) : m(m), m_pinned(m) {} + + void add_production(production* p) { + if (p->is_leaf()) + m_leaves.push_back(p); + else + m_operators.push_back(p); + } + + scoped_ptr_vector const& leaves() const { return m_leaves; } + scoped_ptr_vector const& operators() const { return m_operators; } + ast_manager& mgr() const { return m; } + + void add_func_decl(func_decl *f) { + if (m_seen.contains(f)) + return; + m_pinned.push_back(f); + m_seen.insert(f); + sort_ref range(f->get_range(), m); + sort_ref_vector dom(m); + for (unsigned i = 0; i < f->get_arity(); ++i) + dom.push_back(sort_ref(f->get_domain(i), m)); + add_production(alloc(production, {f->get_name().str(), range, dom, [this, f](expr_ref_vector const &args) { + return expr_ref(m.mk_app(f, args), m); + }})); + } + + void add_expr(expr *e) { + if (m_seen.contains(e)) + return; + m_pinned.push_back(e); + m_seen.insert(e); + sort_ref range(e->get_sort(), m); + sort_ref_vector dom(m); + std::stringstream ss; + ss << mk_bounded_pp(e, m); + std::string name = ss.str(); + add_production(alloc(production, {name, range, dom, [this, e](expr_ref_vector const&) { return expr_ref(e, m); }})); + } + + std::ostream& display(std::ostream& out) const { + out << "Leaves:\n"; + for (auto const *p : m_leaves) { + out << " " << p->name << " : " << mk_pp(p->range, m) << "\n"; + } + out << "Operators:\n"; + for (auto const *p : m_operators) { + out << " " << p->name << " : ("; + for (unsigned i = 0; i < p->domain.size(); ++i) { + if (i > 0) + out << ", "; + out << mk_pp(p->domain[i], m); + } + out << ") -> " << mk_pp(p->range, m) << "\n"; + } + return out; + } + +private: + ast_manager& m; + ast_ref_vector m_pinned; + scoped_ptr_vector m_leaves; + scoped_ptr_vector m_operators; + obj_hashtable m_seen; +}; + +// ============================================================================ +// Term Bank - stores enumerated terms by cost and sort +// ============================================================================ + +using cost_terms = vector>; + +class term_bank { + using sort_term_map = obj_map>; +public: + term_bank(ast_manager& m) : m(m), m_pinned(m) {} + + ~term_bank() { + for (auto s : m_terms) + dealloc(s); + } + + void reset() { + m_pinned.reset(); + m_terms.clear(); + } + + void add(expr* term, unsigned cost) { + sort* s = term->get_sort(); + m_pinned.push_back(term); + if (cost >= m_terms.size()) + m_terms.resize(cost + 1); + if (!m_terms[cost]) + m_terms[cost] = alloc(sort_term_map); + m_terms[cost]->insert_if_not_there(s, ptr_vector()).push_back(term); + } + + /** Get all terms of a given sort up to (and including) max_cost */ + cost_terms get_by_sort(sort* s, unsigned max_cost) const { + cost_terms result; + for (unsigned c = 0; c <= max_cost; ++c) { + if (c >= m_terms.size()) + break; + if (!m_terms[c]->contains(s)) + continue; + for (auto t : m_terms[c]->find(s)) + result.push_back({t, c}); + } + return result; + } + + // Return true if there is at least one term at/above `cost` whose sort is + // not in `sorts` (i.e., enumeration can still produce a new requested sort). + bool is_productive(unsigned cost, uint_set const& sorts) { + for (unsigned i = cost; i < m_terms.size(); ++i) { + if (!m_terms[i]) + continue; + for (auto const& entry : *m_terms[i]) { + sort* term_sort = entry.m_key; + if (!sorts.contains(term_sort->get_small_id())) + return true; + } + } + return false; + } + + ptr_vector null_ptr_vector; + ptr_vector const &get_by_cost_and_sort(unsigned cost, sort *s) const { + if (cost >= m_terms.size() || !m_terms[cost] || !m_terms[cost]->contains(s)) + return null_ptr_vector; + return m_terms[cost]->find(s); + } + + std::ostream& display(std::ostream& out) const { + for (unsigned cost = 0; cost < m_terms.size(); ++cost) { + if (!m_terms[cost]) + continue; + out << "cost " << cost << ":\n"; + for (auto& [s, terms] : *m_terms[cost]) { + out << " sort " << mk_pp(s, m) << ":\n"; + for (expr* e : terms) { + out << " #" << e->get_id() << " "; + if (cost == 0) { + out << mk_bounded_pp(e, m); + } + else if (is_app(e)) { + app* a = to_app(e); + out << a->get_decl()->get_name() << "("; + bool first = true; + for (expr* arg : *a) { + if (!first) out << ", "; + first = false; + out << "#" << arg->get_id(); + } + out << ")"; + } + out << "\n"; + } + } + } + return out; + } + +private: + ast_manager& m; + expr_ref_vector m_pinned; + // cost -> sort -> terms + ptr_vector m_terms; +}; + +// ============================================================================ +// Children Iterator - generates all combinations of child terms +// ============================================================================ + +/** + * Iterates over all tuples (c1, c2, ..., cn) where each ci has the required + * sort, drawn from the term bank, with at least one child at the current + * cost - 1 (to avoid regenerating previously seen terms). + */ +class children_iterator { +public: + children_iterator(ast_manager& m, production const& prod, term_bank const& bank, unsigned current_cost) + : m(m), m_done(false) + { + m_arity = prod.domain.size(); + if (m_arity == 0) { + m_done = true; + return; + } + for (unsigned i = 0; i < m_arity; ++i) { + m_candidates.push_back(bank.get_by_sort(prod.domain[i], current_cost - 1)); + if (m_candidates.back().empty()) { + m_done = true; + return; + } + } + m_indices.resize(m_arity, 0); + } + + bool has_next(unsigned cost) { + while (!m_done) { + if (m.limit().is_canceled()) + return false; + if (has_child_at_cost(cost)) + return true; + advance(); + } + return false; + } + + expr_ref_vector next(unsigned& cost) { + expr_ref_vector result(m); + cost = 1; + for (unsigned i = 0; i < m_arity; ++i) { + auto [e, c] = m_candidates[i].get(m_indices[i]); + cost += c; + result.push_back(e); + } + advance(); + return result; + } + +private: + ast_manager& m; + unsigned m_arity; + bool m_done; + vector m_candidates; + svector m_indices; + + bool has_child_at_cost(unsigned cost) const { + for (unsigned i = 0; i < m_arity; ++i) { + auto [e, c] = m_candidates[i].get(m_indices[i]); + if (c + 1 == cost) + return true; + } + return false; + } + + void advance() { + for (auto i = m_arity; i-- > 0;) { + m_indices[i]++; + if (m_indices[i] < m_candidates[i].size()) return; + m_indices[i] = 0; + } + m_done = true; + } +}; + +// ============================================================================ +// bottom_up_enumerator - the main bottom-up term enumeration engine +// ============================================================================ + + +class bottom_up_enumerator { +public: + bottom_up_enumerator(grammar& grammar) + : m_grammar(grammar), m(grammar.mgr()), + m_bank(grammar.mgr()), m_pending(grammar.mgr()), m_rewriter(grammar.mgr()) + {} + + void set_target_sort(sort *s) { + m_target_sort = s; + } + bool has_next() { + if (m_pending) return true; + m_pending = find_next(); + return m_pending != nullptr; + } + + expr_ref next() { + if (!m_pending) + m_pending = find_next(); + expr_ref result(m_pending, m); + m_pending = nullptr; + return result; + } + + term_bank const& bank() const { return m_bank; } + + std::ostream& display(std::ostream& out) const { + m_grammar.display(out); + return m_bank.display(out); + } + + void reset() { + m_cost = 0; + m_leaf_idx = 0; + m_op_idx = 0; + m_state = State::Leaves; + m_bank.reset(); + m_pending = nullptr; + m_rewriter.reset(); + m_seen_terms.reset(); + m_children_iter.reset(); + } + + expr* add_term(expr_ref const& term, unsigned cost) { + expr_ref simplified(m); + m_rewriter(term, simplified); + if (m_seen_terms.contains(simplified)) + return nullptr; + IF_VERBOSE(10, verbose_stream() << "add " << simplified << "\n"); + m_seen_terms.insert(simplified); + m_bank.add(simplified, cost); + return simplified; + } + +private: + enum class State { Leaves, Operators, Done }; + + grammar& m_grammar; + ast_manager& m; + term_bank m_bank; + unsigned m_cost = 0; + unsigned m_leaf_idx = 0; + unsigned m_op_idx = 0; + unsigned m_bank_idx = 0; + unsigned m_bank_size = 0; + bool m_made_progress = false; + uint_set m_sorts_produced; + State m_state = State::Leaves; + expr_ref m_pending; + th_rewriter m_rewriter; + obj_hashtable m_seen_terms; + std::unique_ptr m_children_iter; + sort *m_target_sort = nullptr; + + bool sort_matches(expr* e) const { + return !m_target_sort || e->get_sort() == m_target_sort; + } + + expr* find_next() { + while (true) { + if (m.limit().is_canceled()) { + m_state = State::Done; + return nullptr; + } + switch (m_state) { + case State::Leaves: + while (m_leaf_idx < m_grammar.leaves().size()) { + production const &prod = *m_grammar.leaves()[m_leaf_idx]; + m_leaf_idx++; + expr_ref_vector empty_args(m); + expr_ref term = prod.builder(empty_args); + expr* r = add_term(term, 0); + if (r && sort_matches(r)) + return r; + } + m_state = State::Operators; + m_cost = 1; + m_op_idx = 0; + m_bank_idx = 0; + m_bank_size = get_bank_size(); + m_made_progress = false; + m_sorts_produced.reset(); + m_children_iter.reset(); + break; + + case State::Operators: { + expr* result = enumerate_operators(); + if (result) + return result; + + m_cost++; + m_op_idx = 0; + m_bank_idx = 0; + m_bank_size = get_bank_size(); + m_children_iter.reset(); + if (!m_made_progress && !m_bank.is_productive(m_cost, m_sorts_produced)) { + m_state = State::Done; + return nullptr; + } + if (m_sorts_produced.contains(m_target_sort->get_small_id())) + m_sorts_produced.reset(); + m_made_progress = false; + break; + } + case State::Done: + return nullptr; + } + } + } + + unsigned get_bank_size() const { + auto const &terms = m_bank.get_by_cost_and_sort(m_cost, m_target_sort); + return terms.size(); + } + + expr *enumerate_operators() { + auto const &ops = m_grammar.operators(); + while (true) { + if (m.limit().is_canceled()) + return nullptr; + + // first find terms at m_cost that were already created + if (m_bank_idx < m_bank_size) { + auto const &terms = m_bank.get_by_cost_and_sort(m_cost, m_target_sort); + auto t = terms.get(m_bank_idx); + m_bank_idx++; + SASSERT(sort_matches(t)); + return t; + } + + // then create new terms using children at cost below current m_cost. + if (m_children_iter && m_children_iter->has_next(m_cost)) { + unsigned new_cost = 0; + expr_ref_vector children = m_children_iter->next(new_cost); + production const &prod = *ops[m_op_idx - 1]; + expr_ref term = prod.builder(children); + // IF_VERBOSE(0, verbose_stream() << term << "\n"); + SASSERT(new_cost >= m_cost); + expr* r = add_term(term, new_cost); + if (!r) + continue; + unsigned sort_id = r->get_sort()->get_small_id(); + if (!m_sorts_produced.contains(sort_id)) + m_made_progress = true; + m_sorts_produced.insert(sort_id); + if (sort_matches(r) && new_cost == m_cost) { + return r; + } + continue; + } + + if (m_op_idx >= ops.size()) + return nullptr; + + production const &prod = *ops[m_op_idx]; + m_op_idx++; + m_children_iter = std::make_unique(m, prod, m_bank, m_cost); + } + } +}; + +} // namespace term_enum + +// ============================================================================ +// term_enumeration public interface implementation +// ============================================================================ + +struct term_enumeration::imp { + ast_manager& m; + term_enum::grammar m_grammar; + term_enum::bottom_up_enumerator m_bottom_up_enumerator; + std::function m_cost; + + imp(ast_manager& m) : + m(m), m_grammar(m), m_bottom_up_enumerator(m_grammar) {} + + void add_production(func_decl* f) { + m_grammar.add_func_decl(f); + } + + void add_production(expr* e) { + m_grammar.add_expr(e); + } + + void set_cost(std::function const& cost) { + // TODO + } + + std::ostream& display(std::ostream& out) const { + return m_bottom_up_enumerator.display(out); + } +}; + +// -- iterator implementation -- + +struct term_enumeration::iterator::iter_imp { + imp& m_imp; + ast_manager & m; + sort* m_sort; + unsigned m_cost = 0; + unsigned m_idx = 0; + vector m_levels; + expr_ref m_current; + bool m_end; + vector m_vars; + vector> m_decls; + vector> m_names; + + iter_imp(imp& i, sort* s) : m_imp(i), m(i.m), m_sort(s), m_current(i.m), m_end(false) { + m_imp.m_bottom_up_enumerator.reset(); + init_sort(); + advance(); + } + + // Sentinel constructor + iter_imp(imp& i) : + m_imp(i), m(i.m), m_sort(nullptr), m_current(i.m), m_end(true) { + UNREACHABLE(); + } + + + void init_sort() { + array_util autil(m); + sort *range = m_sort; + + while (autil.is_array(range)) { + m_vars.push_back(expr_ref_vector(m)); + m_decls.push_back(ptr_vector()); + m_names.push_back(vector()); + for (unsigned i = 0; i < get_array_arity(range); ++i) { + m_decls.back().push_back(get_array_domain(range, i)); + m_vars.back().push_back(nullptr); + m_names.back().push_back(symbol()); + } + + expr_ref_vector args(m); + args.push_back(m.mk_const("a", range)); + for (unsigned i = 0; i < m_decls.back().size(); ++i) { + args.push_back(m.mk_var(i, m_decls.back().get(i))); + } + app_ref sel(autil.mk_select(args), m); + m_imp.m_grammar.add_func_decl(sel->get_decl()); + + range = get_array_range(range); + } + unsigned n = 0; + for (unsigned i = m_decls.size(); i-- > 0;) { + for (unsigned j = m_decls[i].size(); j-- > 0;) { + m_vars[i][j] = m.mk_var(n, m_decls[i][j]); + m_names[i][j] = symbol(n); + m_imp.add_production(m_vars[i].get(j)); + n++; + } + } + m_sort = range; + m_imp.m_bottom_up_enumerator.set_target_sort(range); + } + + void mk_lambda() { + if (!m_current) + return; + for (unsigned i = m_decls.size(); i-- > 0;) + m_current = m.mk_lambda(m_decls[i].size(), m_decls[i].data(), m_names[i].data(), m_current); + } + + void advance() { + if (m_end) + return; + m_current = m_imp.m_bottom_up_enumerator.next(); + SASSERT(!m_current || m_current->get_sort() == m_sort); + mk_lambda(); + if (!m_current) + m_end = true; + } +}; + +term_enumeration::iterator::iterator(imp& i, sort* s) { + m_imp = alloc(iter_imp, i, s); +} + +term_enumeration::iterator::iterator(std::nullptr_t) { + m_imp = nullptr; +} + +term_enumeration::iterator::~iterator() { + dealloc(m_imp); +} + +expr* term_enumeration::iterator::operator*() { + return m_imp ? m_imp->m_current.get() : nullptr; +} + +term_enumeration::iterator& term_enumeration::iterator::operator++() { + if (m_imp) m_imp->advance(); + return *this; +} + +term_enumeration::iterator term_enumeration::iterator::operator++(int) { + iterator tmp(*this); + ++(*this); + return tmp; +} + +bool term_enumeration::iterator::operator==(iterator const& other) const { + if (!m_imp && !other.m_imp) return true; + if (!m_imp) return other.m_imp->m_end; + if (!other.m_imp) return m_imp->m_end; + return m_imp->m_end == other.m_imp->m_end && + m_imp->m_current == other.m_imp->m_current; +} + +// -- terms implementation -- + +term_enumeration::terms::terms(imp* i, sort* s) : m_imp(i), m_sort(s) {} + +term_enumeration::iterator term_enumeration::terms::begin() { + return iterator(*m_imp, m_sort); +} + +term_enumeration::iterator term_enumeration::terms::end() { + return iterator(nullptr); +} + +// -- term_enumeration implementation -- + +term_enumeration::term_enumeration(ast_manager& m) { + m_imp = alloc(imp, m); +} + +term_enumeration::~term_enumeration() { + dealloc(m_imp); +} + +void term_enumeration::add_production(func_decl* f) { + m_imp->add_production(f); +} + +void term_enumeration::add_production(expr* e) { + m_imp->add_production(e); +} + +void term_enumeration::set_cost(std::function const& cost) { + m_imp->set_cost(cost); +} + +term_enumeration::terms term_enumeration::enum_terms(sort* s) { + return terms(m_imp, s); +} + +std::ostream& term_enumeration::display(std::ostream& out) const { + return m_imp->display(out); +} diff --git a/src/ast/rewriter/term_enumeration.h b/src/ast/rewriter/term_enumeration.h new file mode 100644 index 0000000000..865b8d4021 --- /dev/null +++ b/src/ast/rewriter/term_enumeration.h @@ -0,0 +1,50 @@ +#pragma once + +#include "ast/ast.h" +#include + +class term_enumeration { + struct imp; + imp* m_imp; +public: + term_enumeration(ast_manager& m); + ~term_enumeration(); + + void add_production(func_decl* f); + void add_production(expr* e); + // void add_production(sort *s, std::function g); + + // cost function associated with expressions. + // terms are enumerated with increasing cost. + + void set_cost(std::function const& cost); + + class iterator { + struct iter_imp; + iter_imp* m_imp; + public: + iterator(imp& i, sort* s); + iterator(std::nullptr_t); + ~iterator(); + expr* operator*(); + iterator operator++(int); + iterator& operator++(); + bool operator!=(iterator const& other) const { + return !(*this == other); + } + bool operator==(iterator const &other) const; + }; + + class terms { + imp* m_imp; + sort* m_sort; + public: + terms(imp* i, sort* s); + iterator begin(); + iterator end(); + }; + + terms enum_terms(sort* s); + + std::ostream& display(std::ostream& out) const; +}; \ No newline at end of file diff --git a/src/ast/seq_decl_plugin.cpp b/src/ast/seq_decl_plugin.cpp index 5982a1ae2c..8199bae034 100644 --- a/src/ast/seq_decl_plugin.cpp +++ b/src/ast/seq_decl_plugin.cpp @@ -241,7 +241,6 @@ void seq_decl_plugin::init() { m_sigs[OP_RE_OF_PRED] = alloc(psig, m, "re.of.pred", 1, 1, &predA, reA); m_sigs[OP_RE_REVERSE] = alloc(psig, m, "re.reverse", 1, 1, &reA, reA); m_sigs[OP_RE_DERIVATIVE] = alloc(psig, m, "re.derivative", 1, 2, AreA, reA); - m_sigs[_OP_RE_ANTIMIROV_UNION] = alloc(psig, m, "re.union", 1, 2, reAreA, reA); m_sigs[OP_SEQ_TO_RE] = alloc(psig, m, "seq.to.re", 1, 1, &seqA, reA); m_sigs[OP_SEQ_IN_RE] = alloc(psig, m, "seq.in.re", 1, 2, seqAreA, boolT); m_sigs[OP_SEQ_REPLACE_RE_ALL] = alloc(psig, m, "str.replace_re_all", 1, 3, seqAreAseqA, seqA); @@ -413,7 +412,6 @@ func_decl* seq_decl_plugin::mk_func_decl(decl_kind k, unsigned num_parameters, p case OP_RE_COMPLEMENT: case OP_RE_REVERSE: case OP_RE_DERIVATIVE: - case _OP_RE_ANTIMIROV_UNION: m_has_re = true; Z3_fallthrough; case OP_SEQ_UNIT: @@ -423,7 +421,7 @@ func_decl* seq_decl_plugin::mk_func_decl(decl_kind k, unsigned num_parameters, p case OP_STRING_LE: case OP_STRING_IS_DIGIT: case OP_STRING_TO_CODE: - case OP_STRING_FROM_CODE: + case OP_STRING_FROM_CODE: match(*m_sigs[k], arity, domain, range, rng); return m.mk_func_decl(m_sigs[k]->m_name, arity, domain, rng, func_decl_info(m_family_id, k)); @@ -1213,6 +1211,17 @@ app* seq_util::rex::mk_of_pred(expr* p) { return m.mk_app(m_fid, OP_RE_OF_PRED, 0, nullptr, 1, &p); } +app* seq_util::rex::mk_range(sort* re_sort, unsigned lo, unsigned hi) { + if (lo > hi) + return mk_empty(re_sort); + if (lo == 0 && hi == u.max_char()) + return mk_full_char(re_sort); + app* lo_str = u.str.mk_string(zstring(lo)); + if (lo == hi) + return mk_to_re(lo_str); + return mk_range(lo_str, u.str.mk_string(zstring(hi))); +} + bool seq_util::rex::is_loop(expr const* n, expr*& body, unsigned& lo, unsigned& hi) const { if (is_loop(n)) { app const* a = to_app(n); @@ -1441,7 +1450,7 @@ std::ostream& seq_util::rex::pp::print(std::ostream& out, expr* e) const { print(out, r1); print(out, r2); } - else if (re.is_antimirov_union(e, r1, r2) || re.is_union(e, r1, r2)) { + else if (re.is_union(e, r1, r2)) { out << "("; print(out, r1); out << (html_encode ? "⋃" : "|"); @@ -1674,11 +1683,19 @@ seq_util::rex::info seq_util::rex::mk_info_rec(app* e) const { case OP_RE_OPTION: i1 = get_info_rec(e->get_arg(0)); return i1.opt(); - case OP_RE_RANGE: + case OP_RE_RANGE: { + // A concrete range [lo, hi] with lo <= hi is non-empty and classical. + zstring slo, shi; + if (u.str.is_string(e->get_arg(0), slo) && slo.length() == 1 && + u.str.is_string(e->get_arg(1), shi) && shi.length() == 1 && + slo[0] <= shi[0]) + return info(true, l_false, 1, true); + // Symbolic or unknown: not classical + return info(true, l_false, 1, false); + } case OP_RE_FULL_CHAR_SET: case OP_RE_OF_PRED: //TBD: check if the character predicate contains uninterpreted symbols or is nonground or is unsat - //TBD: check if the range is unsat return info(true, l_false, 1, false); case OP_RE_CONCAT: i1 = get_info_rec(e->get_arg(0)); diff --git a/src/ast/seq_decl_plugin.h b/src/ast/seq_decl_plugin.h index 298813de69..681cf3ded4 100644 --- a/src/ast/seq_decl_plugin.h +++ b/src/ast/seq_decl_plugin.h @@ -109,7 +109,6 @@ enum seq_op_kind { _OP_REGEXP_EMPTY, _OP_REGEXP_FULL_CHAR, _OP_RE_IS_NULLABLE, - _OP_RE_ANTIMIROV_UNION, // Lifted union for antimirov-style derivatives _OP_SEQ_SKOLEM, LAST_SEQ_OP }; @@ -525,6 +524,8 @@ public: app* mk_to_re(expr* s) { return m.mk_app(m_fid, OP_SEQ_TO_RE, 1, &s); } app* mk_in_re(expr* s, expr* r) { return m.mk_app(m_fid, OP_SEQ_IN_RE, s, r); } app* mk_range(expr* s1, expr* s2) { return m.mk_app(m_fid, OP_RE_RANGE, s1, s2); } + // Smart constructor: returns re.empty / str.to_re / re.range based on lo vs hi. + app* mk_range(sort* re_sort, unsigned lo, unsigned hi); app* mk_concat(expr* r1, expr* r2) { return m.mk_app(m_fid, OP_RE_CONCAT, r1, r2); } app* mk_union(expr* r1, expr* r2) { return m.mk_app(m_fid, OP_RE_UNION, r1, r2); } app* mk_inter(expr* r1, expr* r2) { return m.mk_app(m_fid, OP_RE_INTERSECT, r1, r2); } @@ -546,7 +547,6 @@ public: app* mk_of_pred(expr* p); app* mk_reverse(expr* r) { return m.mk_app(m_fid, OP_RE_REVERSE, r); } app* mk_derivative(expr* ele, expr* r) { return m.mk_app(m_fid, OP_RE_DERIVATIVE, ele, r); } - app* mk_antimirov_union(expr* r1, expr* r2) { return m.mk_app(m_fid, _OP_RE_ANTIMIROV_UNION, r1, r2); } bool is_to_re(expr const* n) const { return is_app_of(n, m_fid, OP_SEQ_TO_RE); } bool is_concat(expr const* n) const { return is_app_of(n, m_fid, OP_RE_CONCAT); } @@ -580,7 +580,6 @@ public: bool is_of_pred(expr const* n) const { return is_app_of(n, m_fid, OP_RE_OF_PRED); } bool is_reverse(expr const* n) const { return is_app_of(n, m_fid, OP_RE_REVERSE); } bool is_derivative(expr const* n) const { return is_app_of(n, m_fid, OP_RE_DERIVATIVE); } - bool is_antimirov_union(expr const* n) const { return is_app_of(n, m_fid, _OP_RE_ANTIMIROV_UNION); } MATCH_UNARY(is_to_re); MATCH_BINARY(is_concat); MATCH_BINARY(is_union); @@ -595,7 +594,6 @@ public: MATCH_UNARY(is_of_pred); MATCH_UNARY(is_reverse); MATCH_BINARY(is_derivative); - MATCH_BINARY(is_antimirov_union); bool is_loop(expr const* n, expr*& body, unsigned& lo, unsigned& hi) const; bool is_loop(expr const* n, expr*& body, unsigned& lo) const; bool is_loop(expr const* n, expr*& body, expr*& lo, expr*& hi) const; diff --git a/src/ast/simplifiers/dependent_expr_state.h b/src/ast/simplifiers/dependent_expr_state.h index 504f67ad0b..ed34d4a195 100644 --- a/src/ast/simplifiers/dependent_expr_state.h +++ b/src/ast/simplifiers/dependent_expr_state.h @@ -45,7 +45,7 @@ Author: class dependent_expr_state { unsigned m_qhead = 0; bool m_suffix_frozen = false; - unsigned m_num_recfun = 0, m_num_lambdas = 0; + unsigned m_num_recfun = 0; lbool m_has_quantifiers = l_undef; ast_mark m_frozen; func_decl_ref_vector m_frozen_trail; diff --git a/src/ast/simplifiers/elim_unconstrained.cpp b/src/ast/simplifiers/elim_unconstrained.cpp index 6b53882cd5..76a622a456 100644 --- a/src/ast/simplifiers/elim_unconstrained.cpp +++ b/src/ast/simplifiers/elim_unconstrained.cpp @@ -16,7 +16,7 @@ Abstract: All variables x, y, z, .. can eventually be eliminated, but the tactic requires a global analysis between each elimination. We address this by using reference counts and maintaining a heap of reference counts. - - it does not accomodate side constraints. The more general invertibility reduction methods, such + - it does not accommodate side constraints. The more general invertibility reduction methods, such as those introduced for bit-vectors use side constraints. - it is not modular: we detach the expression invertion routines to self-contained code. diff --git a/src/ast/simplifiers/solve_eqs.cpp b/src/ast/simplifiers/solve_eqs.cpp index 0cdb5de699..b4fee6976f 100644 --- a/src/ast/simplifiers/solve_eqs.cpp +++ b/src/ast/simplifiers/solve_eqs.cpp @@ -20,7 +20,7 @@ It traverses the same sub-terms many times. Outline of a presumably better scheme: -1. maintain map FV: term -> bit-set where bitset reprsents set of free variables. Assume the number of variables is bounded. +1. maintain map FV: term -> bit-set where bitset represents set of free variables. Assume the number of variables is bounded. FV is built from initial terms. 2. maintain parent: term -> term-list of parent occurrences. 3. repeat diff --git a/src/cmd_context/cmd_util.h b/src/cmd_context/cmd_util.h index 125ed66549..cec79a26f9 100644 --- a/src/cmd_context/cmd_util.h +++ b/src/cmd_context/cmd_util.h @@ -17,45 +17,51 @@ Notes: --*/ #pragma once -#define ATOMIC_CMD(CLS_NAME, NAME, DESCR, ACTION) \ +#define ATOMIC_CMD(CLS_NAME, NAME, DESCR, ACTION) \ +class CLS_NAME : public cmd { \ +public: \ + CLS_NAME():cmd(NAME) {} \ + char const * get_usage() const override { return 0; } \ + char const * get_descr(cmd_context & ctx) const override { \ + return DESCR; \ + } \ + unsigned get_arity() const override { return 0; } \ + void execute(cmd_context & ctx) override { ACTION } \ +} + +#define UNARY_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \ class CLS_NAME : public cmd { \ public: \ CLS_NAME():cmd(NAME) {} \ - virtual char const * get_usage() const { return 0; } \ - virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \ - virtual unsigned get_arity() const { return 0; } \ - virtual void execute(cmd_context & ctx) { ACTION } \ -}; - -#define UNARY_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \ -class CLS_NAME : public cmd { \ -public: \ - CLS_NAME():cmd(NAME) {} \ - virtual char const * get_usage() const { return USAGE; } \ - virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \ - virtual unsigned get_arity() const { return 1; } \ - virtual cmd_arg_kind next_arg_kind(cmd_context & ctx) const { return ARG_KIND; } \ - virtual void set_next_arg(cmd_context & ctx, ARG_TYPE arg) { ACTION } \ + char const * get_usage() const override { return USAGE; } \ + char const * get_descr(cmd_context & ctx) const override { \ + return DESCR; \ + } \ + unsigned get_arity() const override { return 1; } \ + cmd_arg_kind next_arg_kind(cmd_context & ctx) const override { \ + return ARG_KIND; \ + } \ + void set_next_arg(cmd_context & ctx, ARG_TYPE arg) override { ACTION } \ } // Macro for creating commands where the first argument is a symbol // The second argument cannot be a symbol #define BINARY_SYM_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \ class CLS_NAME : public cmd { \ - symbol m_sym; \ + symbol m_sym; \ public: \ CLS_NAME():cmd(NAME) {} \ - virtual char const * get_usage() const { return USAGE; } \ - virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \ - virtual unsigned get_arity() const { return 2; } \ - virtual void prepare(cmd_context & ctx) { m_sym = symbol::null; } \ - virtual cmd_arg_kind next_arg_kind(cmd_context & ctx) const { \ - return m_sym == symbol::null ? CPK_SYMBOL : ARG_KIND; \ + char const * get_usage() const override { return USAGE; } \ + char const * get_descr(cmd_context & ctx) const override { return DESCR; } \ + unsigned get_arity() const override { return 2; } \ + void prepare(cmd_context & ctx) override { m_sym = symbol::null; } \ + cmd_arg_kind next_arg_kind(cmd_context & ctx) const override { \ + return m_sym == symbol::null ? CPK_SYMBOL : ARG_KIND; \ } \ - virtual void set_next_arg(cmd_context & ctx, symbol const & s) { m_sym = s; } \ - virtual void set_next_arg(cmd_context & ctx, ARG_TYPE arg) { ACTION } \ -}; - + void set_next_arg(cmd_context & ctx, symbol const & s) override { m_sym = s; } \ + void set_next_arg(cmd_context & ctx, ARG_TYPE arg) override { ACTION } \ +} + class ast; class expr; diff --git a/src/cmd_context/tptp_frontend.cpp b/src/cmd_context/tptp_frontend.cpp index c2b2c4fc3f..0f2e686696 100644 --- a/src/cmd_context/tptp_frontend.cpp +++ b/src/cmd_context/tptp_frontend.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include "ast/arith_decl_plugin.h" #include "ast/array_decl_plugin.h" @@ -65,7 +64,8 @@ enum class token_kind { slash_tok, minus_tok, at_tok, - lambda_tok + lambda_tok, + modal_tok }; struct parse_error : public std::exception { @@ -87,6 +87,7 @@ struct token { std::string text; unsigned line = 1; unsigned col = 1; + bool dquote = false; // true for double-quoted strings ("..."): TPTP distinct objects }; class lexer { @@ -163,6 +164,7 @@ public: if (peek() == '\'' || peek() == '"') { char q = get(); t.kind = token_kind::str; + t.dquote = (q == '"'); while (!eof()) { char c = get(); if (c == '\\' && !eof()) { @@ -252,7 +254,7 @@ public: 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.kind = token_kind::modal_tok; t.text.push_back(c); while (!eof() && peek() != '}') t.text.push_back(get()); @@ -277,10 +279,10 @@ public: }; struct parsed_type { - std::vector domain; + ptr_vector domain; sort* range = nullptr; parsed_type(sort* s): range(s) {} - parsed_type(std::vector const& d, sort* r): domain(d), range(r) {} + parsed_type(ptr_vector const& d, sort* r): domain(d), range(r) {} }; class tptp_parser { @@ -290,13 +292,20 @@ class tptp_parser { array_util m_array; sort* m_univ; bool m_has_conjecture = false; + unsigned m_dropped_formulas = 0; // axioms/definitions skipped due to encoding errors bool m_last_name_quoted = false; + bool m_last_name_dquoted = false; // last parsed name was a double-quoted distinct object + // Distinct objects: TPTP double-quoted strings ("...") denote pairwise distinct + // domain elements. Collected here (deduplicated by name) so a single global + // (distinct ...) constraint can be asserted before solving. + std::unordered_map m_distinct_objects; + std::string m_expected_status; // SZS status from the input annotation, if any 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::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 { @@ -424,6 +433,7 @@ class tptp_parser { std::string parse_name() { if (is(token_kind::id) || is(token_kind::str)) { m_last_name_quoted = is(token_kind::str); + m_last_name_dquoted = is(token_kind::str) && m_curr.dquote; std::string r = m_curr.text; next(); return r; @@ -449,7 +459,7 @@ class tptp_parser { // 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* get_ho_sort(ptr_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); @@ -513,7 +523,8 @@ class tptp_parser { 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; + 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); @@ -525,7 +536,7 @@ class tptp_parser { auto itd = m_decls.find(key); if (itd != m_decls.end()) return itd->second; - std::vector dom(arity, m_univ); + ptr_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); @@ -571,6 +582,14 @@ class tptp_parser { expr_ref coerce_arg(expr_ref const& e, sort* target) { sort* actual = e->get_sort(); if (actual == target) return e; + // Int <-> Real conversions must use the arithmetic semantics (to_real / + // to_int), never an uninterpreted boxing function: an uninterpreted box + // severs the numeric link between the two sides and lets the solver build + // spurious models (e.g. it would make floor/ceiling identities sat). + if (m_arith.is_int(actual) && m_arith.is_real(target)) + return expr_ref(m_arith.mk_to_real(e), m); + if (m_arith.is_real(actual) && m_arith.is_int(target)) + return expr_ref(m_arith.mk_to_int(e), m); // 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'); @@ -697,14 +716,14 @@ class tptp_parser { // ::= $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(); + ptr_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; + ptr_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()); + full_domain.append(rhs.domain); } expect(token_kind::rparen, "')'"); // Return with domain/range preserved for proper flattening @@ -743,15 +762,15 @@ class tptp_parser { // Grammar: ::= * // | * // Product types form the domain in mapping types: (A * B) > C - std::vector parse_type_product_raw() { + ptr_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; + ptr_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()); + args.append(t.domain); } else { args.push_back(t.range); } @@ -761,28 +780,28 @@ class tptp_parser { 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; + ptr_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()); + args.append(t.domain); } else { args.push_back(t.range); } } return args; } - std::vector args; + ptr_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()); + args.append(t.domain); } else { args.push_back(t.range); } @@ -801,7 +820,7 @@ class tptp_parser { return first; } // Build product vector - std::vector args; + ptr_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)); @@ -816,7 +835,7 @@ class tptp_parser { 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()); + args.append(t.domain); } else { args.push_back(t.range); } @@ -833,7 +852,7 @@ class tptp_parser { if (is(token_kind::type_forall_tok) || is(token_kind::type_exists_tok)) { next(); expect(token_kind::lbrack, "'['"); - std::vector type_params; + ptr_vector type_params; if (!accept(token_kind::rbrack)) { do { std::string tv = parse_name(); @@ -848,8 +867,8 @@ class tptp_parser { 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()); + ptr_vector full_domain = type_params; + full_domain.append(inner.domain); return parsed_type(full_domain, inner.range); } return inner; @@ -858,7 +877,7 @@ class tptp_parser { 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; + ptr_vector domain; if (!prod.domain.empty() && prod.range == nullptr) { domain = prod.domain; } else if (!prod.domain.empty() && prod.range != nullptr) { @@ -869,7 +888,7 @@ class tptp_parser { } 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()); + domain.append(rhs.domain); return parsed_type(domain, rhs.range); } return parsed_type(domain, rhs.range); @@ -925,7 +944,7 @@ class tptp_parser { // Grammar: (same as parse_term, primary productions) expr_ref parse_term_primary() { if (accept(token_kind::lparen)) { - expr_ref e = parse_formula(); + expr_ref e = parse_formula(false); expect(token_kind::rparen, "')'"); return e; } @@ -946,6 +965,7 @@ class tptp_parser { return parse_numeral_from_name(n); } + bool dq_name = m_last_name_dquoted; expr_ref b(m); // Check bound variables: uppercase (quantifier vars) AND lowercase (let-bound names) if (!m_last_name_quoted && find_bound(n, b)) { @@ -971,11 +991,11 @@ class tptp_parser { // $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()); + args.push_back(parse_formula(true)); expect(token_kind::comma, "','"); - args.push_back(parse_formula()); + args.push_back(parse_formula(false)); expect(token_kind::comma, "','"); - args.push_back(parse_formula()); + args.push_back(parse_formula(false)); expect(token_kind::rparen, "')'"); } else if (n == "$let") { @@ -995,14 +1015,17 @@ class tptp_parser { } 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); + coerce_args(f, args); + expr_ref term(args.empty() ? m.mk_const(f) : m.mk_app(f, args.size(), args.data()), m); + if (dq_name && args.empty()) + register_distinct_object(n, term); + return term; } // Grammar: ::= | // ::= | // Entry point for formula parsing (wraps parse_expr with default precedence). - expr_ref parse_formula(); + expr_ref parse_formula(bool is_boolean); // Grammar: ::= @ // | @ @@ -1042,7 +1065,7 @@ class tptp_parser { return parse_lambda_expr(); } if (accept(token_kind::lparen)) { - expr_ref e = parse_formula(); + expr_ref e = parse_formula(false); expect(token_kind::rparen, "')'"); // Do NOT call apply_at here — outer apply_at owns the remaining @ tokens return e; @@ -1074,7 +1097,7 @@ class tptp_parser { // 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(); + expr_ref body = parse_formula(false); m_in_at_arg = save_in_at_arg; m_bound.pop_back(); return mk_quantifier(is_forall, vars, body); @@ -1092,7 +1115,7 @@ class tptp_parser { 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); + func_decl* f = m.mk_const_decl(name, range); m_pinned_decls.push_back(f); m_decls.emplace(key, f); return f; @@ -1121,31 +1144,29 @@ class tptp_parser { // 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. + // If sorts already match, returns lhs unchanged; otherwise applies minimal + // arithmetic/box coercions so both sides of an equality share a sort. 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; - + // No coercion is needed when both sides already share a sort. In particular + // `=` between two Boolean ($o) operands is logical equivalence (iff): keep + // them Boolean instead of forcing one into the term universe U. + if (lhs->get_sort() == rhs->get_sort()) + return lhs; + if (m_arith.is_int_real(lhs) && m_arith.is_int_real(rhs)) + 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()) { + if (is_app(lhs) && to_app(lhs)->get_num_args() == 0) { 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()) { + if (is_app(rhs) && to_app(rhs)->get_num_args() == 0) { 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); - } + // Prefer coercing to rhs sort, falling back to m_univ + sort* target = rhs->get_sort(); + lhs = coerce_arg(lhs, target); + return lhs; } @@ -1176,7 +1197,7 @@ class tptp_parser { // Bind parameter variables for parsing the RHS if (!param_scope.empty()) m_bound.push_back(param_scope); - expr_ref value = parse_formula(); + expr_ref value = parse_formula(false); if (!param_scope.empty()) m_bound.pop_back(); // For function-style definitions, wrap value in lambdas @@ -1206,7 +1227,7 @@ class tptp_parser { // --- Part 1: Parse type declarations --- std::vector let_names; - std::vector let_sorts; + ptr_vector let_sorts; auto parse_one_typing = [&]() { std::string name = parse_name(); @@ -1264,7 +1285,7 @@ class tptp_parser { // --- Part 3: Parse body with let-bound names in scope --- m_bound.push_back(scope); - expr_ref body = parse_formula(); + expr_ref body = parse_formula(false); m_bound.pop_back(); expect(token_kind::rparen, "')'"); @@ -1288,7 +1309,7 @@ class tptp_parser { // ::= $less | $lesseq | $greater | $greatereq | $is_int | $is_rat | ... // ::= = | != // Also handles: let-bound name resolution, implicit variable creation. - expr_ref parse_atomic_formula() { + expr_ref parse_atomic_formula(bool is_boolean) { 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) || @@ -1307,31 +1328,50 @@ class tptp_parser { token saved = m_curr; next(); if (accept(token_kind::rparen)) { - // Parenthesized connective: treat as HO constant with array sort + // A parenthesized connective used as a higher-order term, e.g. + // "(~) @ p" or "(|) @ p @ q". Encode it as a genuine lambda over Bool + // carrying the real logical semantics, so that application beta-reduces + // to the actual connective (e.g. "(~) @ p" ==> "not p"). Encoding it as + // an uninterpreted array constant instead would sever it from Boolean + // logic and make valid higher-order theorems spuriously + // CounterSatisfiable (the (~)/(|) applications would be unrelated to the + // truth values of their operands). + (void)op_text; 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); + symbol xn("X"), yn("Y"); + if (arity == 1) { + // (~) ==> ^[X:$o] : ~X + expr_ref body(m.mk_not(m.mk_var(0, bool_sort)), m); + return expr_ref(m.mk_lambda(1, &bool_sort, &xn, body), m); } - return expr_ref(m.mk_const(f), m); + // binary connective ==> ^[X:$o] : ^[Y:$o] : (X Y) + // de Bruijn: X is var(1) (outer binder), Y is var(0) (inner binder). + expr* vx = m.mk_var(1, bool_sort); + expr* vy = m.mk_var(0, bool_sort); + expr_ref opbody(m); + switch (saved.kind) { + case token_kind::and_tok: opbody = m.mk_and(vx, vy); break; + case token_kind::or_tok: opbody = m.mk_or(vx, vy); break; + case token_kind::implies_tok: opbody = m.mk_implies(vx, vy); break; + case token_kind::iff_tok: opbody = m.mk_eq(vx, vy); break; + case token_kind::xor_tok: opbody = m.mk_xor(vx, vy); break; + default: opbody = m.mk_eq(vx, vy); break; + } + expr_ref inner(m.mk_lambda(1, &bool_sort, &yn, opbody), m); + return expr_ref(m.mk_lambda(1, &bool_sort, &xn, inner), 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); + // "( ~ )": the '~' is a unary connective binding only the + // next unary unit; ordinary binary connectives then apply at their + // own precedence (e.g. "( ~ p | q )" is "(~p) | q", NOT "~(p | q)"). + // We have already consumed '(' and '~', so negate the next unit and + // resume precedence-climbing parsing from that negated left operand. + expr_ref operand = parse_unary_formula(true); + expr_ref neg(m.mk_not(ensure_bool(operand)), m); + inner = parse_binary_rest(neg, PREC_IFF, true); } else { // Binary connective at start of parens — shouldn't happen in valid TPTP throw parse_error("unexpected connective after '(' at " + loc()); @@ -1342,12 +1382,15 @@ class tptp_parser { // 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(); + expr_ref e = parse_formula(is_boolean); expect(token_kind::rparen, "')'"); m_in_at_arg = save_in_at_arg; return e; } + if (accept(token_kind::modal_tok)) + throw parse_error("modal operators not supported in TPTP input at " + loc()); + // Handle negative numerals in formula position: -2 = $uminus(2) if (accept(token_kind::minus_tok)) { expr_ref t = parse_term(); @@ -1358,9 +1401,9 @@ class tptp_parser { 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(); + expr_ref first = parse_formula(is_boolean); while (accept(token_kind::comma)) - parse_formula(); // consume remaining elements + parse_formula(is_boolean); // consume remaining elements expect(token_kind::rbrack, "']'"); return first; } @@ -1373,6 +1416,7 @@ class tptp_parser { return parse_numeral_from_name(n); } + bool dq_name = m_last_name_dquoted; // Check if name is let-bound (works for both uppercase vars and lowercase let-bound names) { expr_ref b(m); @@ -1417,7 +1461,7 @@ class tptp_parser { } expect(token_kind::colon, "':'"); m_bound.push_back(scope); - expr_ref body = parse_formula(); + expr_ref body = parse_formula(is_boolean); m_bound.pop_back(); // Approximate choice as existential quantification return mk_quantifier(false, vars, body); @@ -1427,11 +1471,11 @@ class tptp_parser { // $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()); + args.push_back(parse_formula(true)); expect(token_kind::comma, "','"); - args.push_back(parse_formula()); + args.push_back(parse_formula(is_boolean)); expect(token_kind::comma, "','"); - args.push_back(parse_formula()); + args.push_back(parse_formula(is_boolean)); expect(token_kind::rparen, "')'"); } else if (n == "$let") { @@ -1465,18 +1509,19 @@ class tptp_parser { 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); + coerce_args(f, args); + return expr_ref(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); - } + is_boolean = is_boolean && !is(token_kind::equal_tok) && !is(token_kind::neq_tok); - 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); + + func_decl* pred = mk_decl_or_ho_const(n, args.size(), is_boolean); + coerce_args(pred, args); + expr_ref atom(m.mk_app(pred, args.size(), args.data()), m); + if (dq_name && args.empty() && !m.is_bool(atom)) + register_distinct_object(n, atom); + return atom; } // Grammar: ::= ^ [] : @@ -1513,7 +1558,7 @@ class tptp_parser { // 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(); + expr_ref body = parse_formula(false); m_in_at_arg = save_in_at_arg; m_bound.pop_back(); if (vars.empty()) @@ -1538,9 +1583,9 @@ class tptp_parser { // ::= // ::= ! | ? // Also handles: $ite, $let, lambda (^), parenthesized formulas, and atomic formulas. - expr_ref parse_unary_formula() { + expr_ref parse_unary_formula(bool is_boolean) { if (accept(token_kind::not_tok)) { - expr_ref e = parse_unary_formula(); + expr_ref e = parse_unary_formula(true); return expr_ref(m.mk_not(ensure_bool(e)), m); } @@ -1552,7 +1597,7 @@ class tptp_parser { next(); // consume '[' if (accept(token_kind::dot)) { expect(token_kind::rbrack, "']'"); - expr_ref sub = parse_unary_formula(); + expr_ref sub = parse_unary_formula(is_boolean); func_decl* f = mk_modal_op("box"); return expr_ref(m.mk_app(f, sub.get()), m); } @@ -1561,7 +1606,7 @@ class tptp_parser { std::string first_name = m_curr.text; next(); if (accept(token_kind::rbrack)) { - expr_ref sub = parse_unary_formula(); + expr_ref sub = parse_unary_formula(is_boolean); func_decl* f = mk_modal_op(mod_name); return expr_ref(m.mk_app(f, sub.get()), m); } @@ -1575,11 +1620,11 @@ class tptp_parser { 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); + func_decl* f = mk_decl_or_ho_const(first_name, 0, is_boolean); first = expr_ref(m.mk_const(f), m); } while (accept(token_kind::comma)) - parse_formula(); // consume remaining elements + parse_formula(is_boolean); // consume remaining elements expect(token_kind::rbrack, "']'"); return first; } @@ -1587,9 +1632,9 @@ class tptp_parser { // 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(); + expr_ref first = parse_formula(is_boolean); while (accept(token_kind::comma)) - parse_formula(); // consume remaining elements + parse_formula(is_boolean); // consume remaining elements expect(token_kind::rbrack, "']'"); return first; } @@ -1605,7 +1650,7 @@ class tptp_parser { next(); } expect(token_kind::gt_tok, "'>'"); - expr_ref sub = parse_unary_formula(); + expr_ref sub = parse_unary_formula(is_boolean); func_decl* f = mk_modal_op(mod_name); return expr_ref(m.mk_app(f, sub.get()), m); } @@ -1652,7 +1697,13 @@ class tptp_parser { } expect(token_kind::colon, "':'"); m_bound.push_back(scope); - expr_ref body = parse_formula(); + // A TPTP quantifier body is a : the quantifier binds + // tighter than the binary connectives & | => <= <=> <~> ~| ~&. Parse + // at equality precedence so the body absorbs an infix =/!= but stops + // at any lower-precedence connective, which stays in the enclosing + // expression. E.g. "! [X] : p(X) & q(X)" is "(! [X] : p(X)) & q(X)", + // and "! [X] : (...) => g" keeps "=> g" outside the quantifier scope. + expr_ref body = parse_expr(PREC_EQ, true, is_boolean); m_bound.pop_back(); return mk_quantifier(is_forall, vars, body); } @@ -1672,10 +1723,10 @@ class tptp_parser { expect(token_kind::rbrack, "']'"); } expect(token_kind::colon, "':'"); - return parse_formula(); + return parse_formula(is_boolean); } - return parse_atomic_formula(); + return parse_atomic_formula(is_boolean); } // Grammar: ::= | @@ -1687,8 +1738,15 @@ class tptp_parser { // ::= & // | & // 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(); + expr_ref parse_expr(unsigned min_prec, bool consume_at, bool is_boolean) { + expr_ref e = parse_unary_formula(is_boolean); + return parse_binary_rest(e, min_prec, consume_at); + } + + // Precedence-climbing loop continued from an already-parsed left operand `e`. + // Split out from parse_expr so callers that have consumed a leading unary unit + // (e.g. a '~' immediately after '(') can resume binary-connective parsing. + expr_ref parse_binary_rest(expr_ref e, unsigned min_prec, bool consume_at = true) { for (;;) { // Handle @ (function application) with highest precedence // But NOT when we're inside a lambda body that's an @ argument @@ -1719,7 +1777,15 @@ class tptp_parser { 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); + // Operands of every connective except '='/'!=' are Boolean; only equality + // takes term operands. Derive the operand context from the operator rather + // than inheriting is_boolean from the left operand. Otherwise, once a + // term-valued equality literal (e.g. 'a = b') sets is_boolean to false, a + // predicate atom later in the same clause ('a = b | q') would be parsed as a + // term and boxed into '$box_U_to_Bool(q)', severing it from the Boolean + // predicate 'q' used elsewhere and making refutable problems satisfiable. + bool rhs_is_boolean = it->second.precedence != PREC_EQ; + expr_ref rhs = parse_expr(next_prec, consume_at, rhs_is_boolean); expr_ref_vector args(m); args.push_back(e); args.push_back(rhs); @@ -1786,12 +1852,27 @@ class tptp_parser { // 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) + // Try TPTP environment variable (standard TPTP convention): includes such as + // "Axioms/MAT001^0.ax" are resolved relative to the TPTP root directory named + // by $TPTP. This is required when a problem is run from a directory that does + // not contain the Axioms/ tree (e.g. an isolated benchmark harness workspace). char const* root = std::getenv("TPTP"); if (root) { std::string env = normalize_path(std::string(root) + "/" + name); if (file_exists(env)) return env; } + // Walk up ancestor directories of the current file. TPTP include paths are + // relative to the TPTP root directory (e.g. "Axioms/BOO001-0.ax"), while the + // problem file typically lives in a subdirectory such as "Problems/BOO/". + std::string dir = dirname(curr_file); + for (;;) { + size_t idx = dir.find_last_of("/\\"); + if (idx == std::string::npos) break; + dir = dir.substr(0, idx); + if (dir.empty()) break; + std::string candidate = normalize_path(dir + "/" + name); + if (file_exists(candidate)) return candidate; + } // 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; @@ -1825,7 +1906,7 @@ class tptp_parser { // ::= , | void parse_annotated() { expect(token_kind::lparen, "'('"); - parse_name(); + std::string formula_name = parse_name(); expect(token_kind::comma, "','"); std::string role = to_lower(parse_name()); expect(token_kind::comma, "','"); @@ -1836,12 +1917,14 @@ class tptp_parser { else if (role == "logic") { // Modal logic declarations ($modal == [...]) — skip the formula body skip_annotations_until_rparen(); + warning_msg("non-classical logics are not supported"); + ++m_dropped_formulas; } else { try { implicit_var_scope implicit_scope; scoped_implicit_vars scoped(*this, implicit_scope); - expr_ref f = parse_formula(); + expr_ref f = parse_formula(true); if (!implicit_scope.order.empty()) { f = mk_quantifier(true, implicit_scope.order, f); } @@ -1854,13 +1937,30 @@ class tptp_parser { } 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"); + // Sort mismatch or other semantic error in this formula — skip it. + // A dropped axiom/definition removes constraints from the problem, so a + // subsequent "sat" verdict is unsound: it may only hold because the + // dropped formula was missing. The count is used to downgrade a sat + // result to GaveUp rather than report a spurious CounterSatisfiable. + ++m_dropped_formulas; + std::ostringstream oss; + oss << "skipping formula '" << formula_name << "' due to: " << ex.what(); + warning_msg(oss.str().c_str()); // 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; + } catch (std::exception const& ex) { + ++m_dropped_formulas; + std::ostringstream oss; + oss << "skipping formula '" << formula_name << "' (role " << role << ") due to: " << ex.what() << "\n"; + warning_msg(oss.str().c_str()); + while (!is(token_kind::eof_tok) && !is(token_kind::dot)) + next(); + if (is(token_kind::dot)) + next(); + return; } } @@ -2114,6 +2214,12 @@ public: }}; } + // Record a double-quoted string constant as a TPTP distinct object (deduplicated by name). + void register_distinct_object(std::string const& name, expr* c) { + if (m_distinct_objects.emplace(name, c).second) + m_pinned_exprs.push_back(c); + } + 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); @@ -2123,6 +2229,7 @@ public: std::ostringstream buf; buf << in.rdbuf(); m_input = buf.str(); + extract_expected_status(m_input); m_lex = std::make_unique(m_input); next(); parse_toplevel(current_file); @@ -2149,6 +2256,78 @@ public: } bool has_conjecture() const { return m_has_conjecture; } + + // TPTP double-quoted strings ("...") denote pairwise distinct domain elements. + // Assert a single global distinctness constraint over all collected distinct objects + // so that e.g. "Apple" != "Microsoft" is recognized as a theorem. + void assert_distinct_objects() { + if (m_distinct_objects.size() < 2) return; + expr_ref_vector objs(m); + for (auto const& kv : m_distinct_objects) + objs.push_back(kv.second); + m_cmd.assert_expr(expr_ref(m.mk_distinct(objs.size(), objs.data()), m)); + } + + // Number of axioms/definitions that were dropped during parsing because the + // higher-order encoding could not type-check them. When non-zero, a "sat" + // verdict cannot be trusted (the missing constraints may be exactly what + // makes the problem unsatisfiable). + unsigned dropped_formulas() const { return m_dropped_formulas; } + + std::string const& expected_status() const { return m_expected_status; } + + // Scan TPTP comments for an SZS/Status annotation, e.g. + // % Status : Unsatisfiable + // % SZS status Theorem + // Only the first annotation found (the top-level file's) is recorded. + void extract_expected_status(std::string const& text) { + if (!m_expected_status.empty()) + return; + std::istringstream in(text); + std::string line; + while (std::getline(in, line)) { + // TPTP comment lines start with '%'. + size_t i = line.find_first_not_of(" \t"); + if (i == std::string::npos || line[i] != '%') + continue; + ++i; // skip '%' + // Skip leading '%' and spaces. + i = line.find_first_not_of("% \t", i); + if (i == std::string::npos) + continue; + std::string rest = line.substr(i); + std::string status; + // Form 1: "SZS status " + if (rest.compare(0, 4, "SZS ") == 0) { + size_t p = rest.find("status"); + if (p == std::string::npos) + continue; + p += 6; // length of "status" + status = next_status_word(rest, p); + } + // Form 2: "Status : " / "Status : " + else if (rest.compare(0, 6, "Status") == 0) { + size_t p = rest.find(':', 6); + if (p == std::string::npos) + continue; + status = next_status_word(rest, p + 1); + } + if (!status.empty()) { + m_expected_status = status; + return; + } + } + } + + static std::string next_status_word(std::string const& s, size_t p) { + size_t a = s.find_first_not_of(" \t", p); + if (a == std::string::npos) + return ""; + size_t b = a; + while (b < s.size() && (isalnum((unsigned char)s[b]) || s[b] == '_')) + ++b; + return s.substr(a, b - a); + } }; expr_ref tptp_parser::parse_term() { @@ -2172,12 +2351,41 @@ expr_ref tptp_parser::parse_term() { return e; } -expr_ref tptp_parser::parse_formula() { - return parse_expr(PREC_IFF); +expr_ref tptp_parser::parse_formula(bool is_boolean) { + return parse_expr(PREC_IFF, true, is_boolean); } } +// Classify an SZS status into the coarse verdict used for cross-checking. +// unsat: a refutation/proof exists (Theorem, Unsatisfiable, ContradictoryAxioms, ...) +// sat: a model exists (Satisfiable, CounterSatisfiable, ...) +// other: no comparable verdict (Open, Unknown, Timeout, GaveUp, empty, ...) +enum class szs_verdict { unsat, sat, other }; + +static szs_verdict classify_szs(std::string const& s) { + if (s == "Theorem" || s == "Unsatisfiable" || s == "ContradictoryAxioms" || s == "Unsat") + return szs_verdict::unsat; + if (s == "Satisfiable" || s == "CounterSatisfiable" || s == "CounterTheorem" || s == "Sat") + return szs_verdict::sat; + return szs_verdict::other; +} + +// Emit the SZS status produced by z3. If the input carries an annotated status +// that contradicts the produced verdict, prepend "BUG" to flag the mismatch. +static void report_szs_status(char const* produced, std::string const& expected) { + szs_verdict pv = classify_szs(produced); + szs_verdict ev = classify_szs(expected); + bool is_bug = !expected.empty() && + (pv == szs_verdict::unsat || pv == szs_verdict::sat) && + (ev == szs_verdict::unsat || ev == szs_verdict::sat) && + pv != ev; + if (is_bug) + std::cout << "% SZS status BUG " << produced << " (expected " << expected << ")\n"; + else + std::cout << "% SZS status " << produced << "\n"; +} + static unsigned read_tptp_stream(std::istream& in, char const* current_file) { register_on_timeout_proc(on_timeout); try { @@ -2186,6 +2394,7 @@ static unsigned read_tptp_stream(std::istream& in, char const* current_file) { tptp_parser p(ctx); p.parse_input(in, current_file ? current_file : "."); + p.assert_distinct_objects(); // Suppress default check-sat output; TPTP frontend reports SZS status explicitly. std::ostringstream sink; @@ -2194,12 +2403,24 @@ static unsigned read_tptp_stream(std::istream& in, char const* current_file) { 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"; + if (p.has_conjecture()) report_szs_status("Theorem", p.expected_status()); + else report_szs_status("Unsatisfiable", p.expected_status()); break; case cmd_context::css_sat: - if (p.has_conjecture()) std::cout << "% SZS status CounterSatisfiable\n"; - else std::cout << "% SZS status Satisfiable\n"; + // A "sat" verdict is only sound if the whole problem was encoded. If any + // axiom/definition was dropped during parsing (e.g. an unsupported + // higher-order construct), the model may be spurious — the dropped + // constraints could rule it out. Report GaveUp instead of a misleading + // CounterSatisfiable/Satisfiable (which would otherwise be flagged BUG + // against an annotated Theorem/Unsatisfiable status). + if (p.dropped_formulas() > 0) { + std::cout << "% SZS status GaveUp\n"; + std::cout << "% SZS reason " << p.dropped_formulas() + << " formula(s) dropped during encoding; model is not certified\n"; + break; + } + if (p.has_conjecture()) report_szs_status("CounterSatisfiable", p.expected_status()); + else report_szs_status("Satisfiable", p.expected_status()); if (g_display_model) { model_ref mdl; if (ctx.is_model_available(mdl)) diff --git a/src/math/lp/bound_analyzer_on_row.h b/src/math/lp/bound_analyzer_on_row.h index 70358ca7b8..6c2787d13d 100644 --- a/src/math/lp/bound_analyzer_on_row.h +++ b/src/math/lp/bound_analyzer_on_row.h @@ -297,7 +297,7 @@ namespace lp { void limit_j(unsigned bound_j, const mpq& u, bool coeff_before_j_is_pos, bool is_lower_bound, bool strict) { auto* lar = &m_bp.lp(); - const auto& row = this->m_row; + auto* row = &this->m_row; auto explain = [row, bound_j, coeff_before_j_is_pos, is_lower_bound, strict, lar]() { (void) strict; TRACE(bound_analyzer, tout << "explain_bound_on_var_on_coeff, bound_j = " << bound_j << ", coeff_before_j_is_pos = " << coeff_before_j_is_pos << ", is_lower_bound = " << is_lower_bound << ", strict = " << strict << "\n";); @@ -305,7 +305,7 @@ namespace lp { int j_sign = (coeff_before_j_is_pos ? 1 : -1) * bound_sign; u_dependency* ret = nullptr; - for (auto const& r : row) { + for (auto const& r : *row) { unsigned j = r.var(); if (j == bound_j) continue; diff --git a/src/math/lp/dioph_eq.cpp b/src/math/lp/dioph_eq.cpp index 0b3c5166a5..c6719e0e38 100644 --- a/src/math/lp/dioph_eq.cpp +++ b/src/math/lp/dioph_eq.cpp @@ -580,7 +580,7 @@ namespace lp { const lar_term* m_t; undo_add_term(imp& s, const lar_term* t) : m_s(s), m_t(t) {} - void undo() { + void undo() override { m_s.undo_add_term_method(m_t); } }; diff --git a/src/math/lp/int_cube.cpp b/src/math/lp/int_cube.cpp index aad7fae6ef..52eabf9fed 100644 --- a/src/math/lp/int_cube.cpp +++ b/src/math/lp/int_cube.cpp @@ -16,6 +16,9 @@ Author: Revision History: --*/ +#include +#include + #include "math/lp/int_solver.h" #include "math/lp/lar_solver.h" #include "math/lp/int_cube.h" @@ -81,32 +84,264 @@ namespace lp { SASSERT(lp_status::OPTIMAL == lra.get_status() || lp_status::FEASIBLE == lra.get_status()); } - impq int_cube::get_cube_delta_for_term(const lar_term& t) const { - if (t.size() == 2) { - bool seen_minus = false; - bool seen_plus = false; - for(lar_term::ival p : t) { - if (!lia.column_is_int(p.j())) - goto usual_delta; - const mpq & c = p.coeff(); - if (c == one_of_type()) { - seen_plus = true; - } else if (c == -one_of_type()) { - seen_minus = true; - } else { - goto usual_delta; + // The largest cube test of Bromberger and Weidenbach: + // maximize x_e subject to Ax + a'(x_e/2) <= b, x_e >= 0, where a'_i = ||a_i||_1, + // with the 1-norm taken over the integer variables of the row. + // The solution is the center z of a largest cube contained in the polyhedron. + // If the maximal edge length is at least 1, then the rounding of z is + // an integer solution; otherwise the rounding is checked, and possibly repaired, + // against the original constraints. + lia_move int_cube::find_largest_cube() { + lia.settings().stats().m_lcube_calls++; + TRACE(cube, + for (unsigned j = 0; j < lra.number_of_vars(); ++j) + lia.display_column(tout, j); + tout << lra.constraints(); + ); + + lra.push(); + // The edge rows are ephemeral: suppress the add-term callback, + // dioph_eq's reaction to it is not undone by pop(). + auto add_term_cb = lra.m_add_term_callback; + lra.m_add_term_callback = nullptr; + unsigned x_e = lra.add_var(UINT_MAX, false); // the edge length of the cube + lra.add_var_bound(x_e, lconstraint_kind::GE, mpq(0)); + bool ok = add_cube_edge_rows(x_e); + lra.m_add_term_callback = add_term_cb; + if (!ok) { + lra.pop(); + lra.set_status(lp_status::OPTIMAL); + return lia_move::undef; + } + + lp_status st = lra.find_feasible_solution(); + if (st != lp_status::FEASIBLE && st != lp_status::OPTIMAL) { + TRACE(cube, tout << "cannot find a feasible solution";); + lra.pop(); + lra.move_non_basic_columns_to_bounds(); + // it can happen that we found an integer solution here + return !lra.r_basis_has_inf_int()? lia_move::sat: lia_move::undef; + } + + impq e; // the maximal edge length + st = lra.maximize_term(x_e, e, /*fix_int_cols*/ false); + if (lia.settings().get_cancel_flag()) { + lra.pop(); + return lia_move::undef; + } + if (st == lp_status::UNBOUNDED) { + // infinite lattice width: the polyhedron contains cubes of arbitrary edge length + lra.add_var_bound(x_e, lconstraint_kind::GE, mpq(1)); + st = lra.find_feasible_solution(); + if (st != lp_status::FEASIBLE && st != lp_status::OPTIMAL) { + lra.pop(); + return lia_move::undef; + } + lra.pop(); + return sat_after_rounding(); + } + TRACE(cube, tout << "max edge length = " << e << "\n";); + if (e >= impq(mpq(1))) { + lra.pop(); + return sat_after_rounding(); + } + // the largest cube is smaller than the unit cube: + // the rounded center is only a candidate + lra.pop(); + return round_and_repair(); + } + + bool int_cube::add_cube_edge_rows(unsigned x_e) { + // snapshot the term columns: add_edge_rows_for_term appends to lra.terms() + svector term_columns; + for (const lar_term* t : lra.terms()) + term_columns.push_back(t->j()); + for (unsigned j : term_columns) + if (!add_edge_rows_for_term(j, x_e)) { + TRACE(cube, tout << "cannot add the edge rows";); + return false; + } + return true; + } + + // i is the column index having the term + bool int_cube::add_edge_rows_for_term(unsigned i, unsigned x_e) { + if (!lra.column_associated_with_row(i)) + return true; + const lar_term& t = lra.get_term(i); + impq delta = get_cube_delta_for_term(t); + TRACE(cube, lra.print_term_as_indices(t, tout); tout << ", delta = " << delta << "\n";); + if (is_zero(delta)) + return true; + if (!is_zero(delta.y)) + // the infinitesimal delta does not scale with x_e: tighten statically, + // it is sound for any edge length + return lra.tighten_term_bounds_by_delta(i, delta); + if (lra.column_has_upper_bound(i)) { + impq u = lra.get_upper_bound(i); // copy: add_term invalidates bound references + vector> coeffs = {{mpq(1), i}, {delta.x, x_e}}; + unsigned s = lra.add_term(coeffs, UINT_MAX); + lra.add_var_bound(s, is_zero(u.y) ? lconstraint_kind::LE : lconstraint_kind::LT, u.x); + } + if (lra.column_has_lower_bound(i)) { + impq l = lra.get_lower_bound(i); // copy: add_term invalidates bound references + vector> coeffs = {{mpq(1), i}, {-delta.x, x_e}}; + unsigned s = lra.add_term(coeffs, UINT_MAX); + lra.add_var_bound(s, is_zero(l.y) ? lconstraint_kind::GE : lconstraint_kind::GT, l.x); + } + return true; + } + + lia_move int_cube::sat_after_rounding() { + lra.round_to_integer_solution(); + lra.set_status(lp_status::FEASIBLE); + SASSERT(lia.settings().get_cancel_flag() || lia.is_feasible()); + TRACE(cube, tout << "largest cube success";); + lia.settings().stats().m_lcube_success++; + return lia_move::sat; + } + + lia_move int_cube::round_and_repair() { + lra.backup_x(); // remember the cube center + vector flips; + for (unsigned j = 0; j < lra.column_count(); ++j) { + if (!lra.column_is_int(j) || lra.column_has_term(j)) + continue; + const impq& v = lra.get_column_value(j); + if (v.is_int()) + continue; + flips.push_back({j, floor(v), false}); + } + lra.round_to_integer_solution(); + for (auto& f : flips) + f.m_at_hi = lra.get_column_value(f.m_j).x > f.m_lo; + if (repair_rounded_candidate(flips)) { + lra.set_status(lp_status::FEASIBLE); + SASSERT(lia.settings().get_cancel_flag() || lia.is_feasible()); + TRACE(cube, tout << "largest cube success";); + lia.settings().stats().m_lcube_success++; + return lia_move::sat; + } + // return to the cube center: an interior point of the polyhedron + lra.restore_x(); + lra.set_status(lp_status::FEASIBLE); + return lia_move::undef; + } + + // Checks the rounded center against the original constraints. On failure + // searches the vertices of the lattice cell around the center greedily: + // flip a coordinate between floor and ceiling to maximally decrease the + // total bound violation, within a budget. + bool int_cube::repair_rounded_candidate(vector& flips) { + vector rows; + for (const lar_term* t : lra.terms()) { + unsigned j = t->j(); + if (!lra.column_associated_with_row(j)) + continue; + if (!lra.column_has_upper_bound(j) && !lra.column_has_lower_bound(j)) + continue; + bounded_row r; + r.m_j = j; + r.m_val = t->apply(lra.r_x()); + rows.push_back(r); + } + auto row_violation = [&](unsigned ri, const impq& v) { + impq w; + unsigned j = rows[ri].m_j; + if (lra.column_has_upper_bound(j) && v > lra.get_upper_bound(j)) + w += v - lra.get_upper_bound(j); + if (lra.column_has_lower_bound(j) && v < lra.get_lower_bound(j)) + w += lra.get_lower_bound(j) - v; + return w; + }; + impq violation; + for (unsigned ri = 0; ri < rows.size(); ++ri) + violation += row_violation(ri, rows[ri].m_val); + if (is_zero(violation)) + return true; // the rounded center fits as it is + if (flips.empty()) + return false; + + std::unordered_map flip_of_var; + for (unsigned fi = 0; fi < flips.size(); ++fi) + flip_of_var[flips[fi].m_j] = fi; + // occurrences of the flip candidates in the bounded rows + vector>> occs(flips.size()); + for (unsigned ri = 0; ri < rows.size(); ++ri) { + const lar_term& t = lra.get_term(rows[ri].m_j); + for (lar_term::ival p : t) { + auto it = flip_of_var.find(p.j()); + if (it != flip_of_var.end()) + occs[it->second].push_back({ri, p.coeff()}); + } + } + + unsigned budget = std::min(2 * flips.size(), lia.settings().lcube_flips()); + bool flipped = false; + while (!is_zero(violation) && budget-- > 0) { + unsigned best_fi = UINT_MAX; + impq best_gain; + for (unsigned fi = 0; fi < flips.size(); ++fi) { + if (occs[fi].empty()) + continue; + mpq step = flips[fi].m_at_hi ? mpq(-1) : mpq(1); + impq gain; + for (const auto& o : occs[fi]) { + const impq& v = rows[o.first].m_val; + gain += row_violation(o.first, v + impq(step * o.second)) - row_violation(o.first, v); + } + if (gain < best_gain) { + best_gain = gain; + best_fi = fi; } } - if (seen_minus && seen_plus) - return zero_of_type(); - return impq(0, 1); + if (best_fi == UINT_MAX) + return false; // no flip decreases the violation + mpq step = flips[best_fi].m_at_hi ? mpq(-1) : mpq(1); + for (const auto& o : occs[best_fi]) + rows[o.first].m_val += impq(step * o.second); + flips[best_fi].m_at_hi = !flips[best_fi].m_at_hi; + violation += best_gain; + flipped = true; + TRACE(cube, tout << "flipped column " << flips[best_fi].m_j << ", violation = " << violation << "\n";); + } + if (!is_zero(violation)) + return false; + + // apply the repaired candidate + for (const auto& f : flips) + lra.set_column_value(f.m_j, impq(f.m_at_hi ? f.m_lo + 1 : f.m_lo)); + for (const lar_term* t : lra.terms()) { + unsigned j = t->j(); + if (!lra.column_associated_with_row(j)) + continue; + lra.set_column_value(j, t->apply(lra.r_x())); + } + if (flipped) + lia.settings().stats().m_lcube_flip_success++; + return true; + } + + impq int_cube::get_cube_delta_for_term(const lar_term& t) const { + if (t.size() == 2) { + bool seen_minus = false, seen_plus = false, all_ok = true; + for (lar_term::ival p : t) { + if (!lia.column_is_int(p.j())) { all_ok = false; break; } + const mpq& c = p.coeff(); + if (c == one_of_type()) seen_plus = true; + else if (c == -one_of_type()) seen_minus = true; + else { all_ok = false; break; } + } + if (all_ok) { + if (seen_minus && seen_plus) + return zero_of_type(); + return impq(0, 1); + } } - usual_delta: mpq delta = zero_of_type(); for (lar_term::ival p : t) if (lia.column_is_int(p.j())) delta += abs(p.coeff()); - delta *= mpq(1, 2); return impq(delta); } diff --git a/src/math/lp/int_cube.h b/src/math/lp/int_cube.h index 4addc096b5..a7d672a52b 100644 --- a/src/math/lp/int_cube.h +++ b/src/math/lp/int_cube.h @@ -10,9 +10,15 @@ Abstract: Cube finder This routine attempts to find a feasible integer solution - by tightnening bounds and running an LRA solver on the + by tightnening bounds and running an LRA solver on the tighter system. + find_largest_cube() implements the largest cube test of + Bromberger and Weidenbach (Fast Cube Tests for LIA Constraint + Solving, IJCAR 2016): a fresh variable x_e for the cube edge + length is introduced and maximized; the center of the largest + cube is rounded to a candidate integer solution. + Author: Nikolaj Bjorner (nbjorner) Lev Nachmanson (levnach) @@ -21,7 +27,10 @@ Revision History: --*/ #pragma once +#include "util/vector.h" #include "math/lp/lia_move.h" +#include "math/lp/numeric_pair.h" +#include "math/lp/lar_term.h" namespace lp { class int_solver; @@ -29,12 +38,30 @@ namespace lp { class int_cube { class int_solver& lia; class lar_solver& lra; + // a fractional integer coordinate of the cube center: + // the candidate value is m_lo or m_lo + 1 + struct flip_candidate { + unsigned m_j = 0; + mpq m_lo; + bool m_at_hi = false; + }; + // a term column with at least one bound, tracked during the repair + struct bounded_row { + unsigned m_j = 0; + impq m_val; + }; bool tighten_term_for_cube(unsigned i); bool tighten_terms_for_cube(); void find_feasible_solution(); impq get_cube_delta_for_term(const lar_term& t) const; + bool add_edge_rows_for_term(unsigned i, unsigned x_e); + bool add_cube_edge_rows(unsigned x_e); + lia_move sat_after_rounding(); + lia_move round_and_repair(); + bool repair_rounded_candidate(vector& flips); public: int_cube(int_solver& lia); lia_move operator()(); + lia_move find_largest_cube(); }; } diff --git a/src/math/lp/int_solver.cpp b/src/math/lp/int_solver.cpp index cf08785974..287a57fd06 100644 --- a/src/math/lp/int_solver.cpp +++ b/src/math/lp/int_solver.cpp @@ -44,7 +44,11 @@ namespace lp { dioph_eq m_dio; int_gcd_test m_gcd; unsigned m_initial_dio_calls_period; - + unsigned m_lcube_period; + // The number of consecutive genuine dio calls that returned undef, reset on a dio + // conflict. Drives the decision to start running Gomory with dio. + unsigned m_dio_undef_in_a_row = 0; + bool column_is_int_inf(unsigned j) const { return lra.column_is_int(j) && (!lia.value_is_int(j)); } @@ -52,7 +56,8 @@ namespace lp { imp(int_solver& lia): lia(lia), lra(lia.lra), lrac(lia.lrac), m_hnf_cutter(lia), m_dio(lia), m_gcd(lia) { m_hnf_cut_period = settings().hnf_cut_period(); m_initial_dio_calls_period = settings().dio_calls_period(); - } + m_lcube_period = settings().m_int_find_cube_period; + } bool has_lower(unsigned j) const { switch (lrac.m_column_types()[j]) { @@ -176,14 +181,16 @@ namespace lp { if (r == lia_move::conflict) { m_dio.explain(*this->m_ex); lia.settings().dio_calls_period() = m_initial_dio_calls_period; - lia.settings().dio_enable_gomory_cuts() = false; + m_dio_undef_in_a_row = 0; + lia.settings().stop_running_gomory_with_dio(); // dio was productive: stop running Gomory lia.settings().set_run_gcd_test(false); return lia_move::conflict; } if (r == lia_move::undef) { - lia.settings().dio_calls_period() *= 2; - if (lra.settings().dio_calls_period() >= 16) { - lia.settings().dio_enable_gomory_cuts() = true; + lia.settings().dio_calls_period() *= 2; // throttle dio scheduling on failure + ++m_dio_undef_in_a_row; + if (m_dio_undef_in_a_row >= lra.settings().dio_gomory_enable_period()) { + lia.settings().start_running_gomory_with_dio(); // dio persistently unproductive: start running Gomory lia.settings().set_run_gcd_test(true); } } @@ -191,26 +198,66 @@ namespace lp { } lp_settings& settings() { return lra.settings(); } + + // Decide whether a periodic heuristic fires on this call. When + // random_hammers is enabled the gate is drawn at random with the same + // 1/period expected rate instead of a deterministic "every k-th call" + // modulus: a deterministic period can phase-lock with the search on + // some families and drown the solver in conflicts while another handler + // is starved; randomizing the gate breaks that resonance. + bool hit_period(unsigned period) { + if (period <= 1) + return true; + if (settings().random_hammers()) + return settings().random_next(period) == 0; + return m_number_of_calls % period == 0; + } + bool should_find_cube() { - return m_number_of_calls % settings().m_int_find_cube_period == 0; + return hit_period(settings().m_int_find_cube_period); + } + + // The largest cube test is throttled exponentially: when the polyhedron + // does not contain a large enough cube it is unlikely to contain one + // later, after more constraints are added, so each failure doubles the + // period and a success resets it. + bool should_find_lcube() { + return settings().lcube() && hit_period(m_lcube_period); + } + + lia_move find_lcube() { + lia_move r = int_cube(lia).find_largest_cube(); + if (r == lia_move::undef) { + if (m_lcube_period < (1u << 30)) + m_lcube_period *= 2; + } + else + m_lcube_period = settings().m_int_find_cube_period; + return r; } bool should_gomory_cut() { bool dio_allows_gomory = !settings().dio() || settings().dio_enable_gomory_cuts() || m_dio.some_terms_are_ignored(); - return dio_allows_gomory && m_number_of_calls % settings().m_int_gomory_cut_period == 0; + return dio_allows_gomory && hit_period(settings().m_int_gomory_cut_period); } bool should_solve_dioph_eq() { - return lia.settings().dio() && (m_number_of_calls % settings().dio_calls_period() == 0); + bool ret = lia.settings().dio() && hit_period(settings().dio_calls_period()); + if (!ret && lia.settings().dio_calls_period() > m_initial_dio_calls_period) { + unsigned dec = settings().dio_calls_period_decrease(); + unsigned& period = lia.settings().dio_calls_period(); + period = period > m_initial_dio_calls_period + dec ? period - dec : m_initial_dio_calls_period; + } + return ret; } // HNF bool should_hnf_cut() { return (!settings().dio() || settings().dio_enable_hnf_cuts()) - && settings().enable_hnf() && m_number_of_calls % settings().hnf_cut_period() == 0; + && settings().enable_hnf() && hit_period(settings().hnf_cut_period()); } lia_move hnf_cut() { @@ -245,11 +292,12 @@ namespace lp { ++m_number_of_calls; if (r == lia_move::undef) r = patch_basic_columns(); - if (r == lia_move::undef && should_find_cube()) r = int_cube(lia)(); + if (r == lia_move::undef && should_find_cube()) r = int_cube(lia)(); + if (r == lia_move::undef && should_find_lcube()) r = find_lcube(); if (r == lia_move::undef) lra.move_non_basic_columns_to_bounds(); if (r == lia_move::undef && should_hnf_cut()) r = hnf_cut(); - if (r == lia_move::undef && should_gomory_cut()) r = gomory(lia).get_gomory_cuts(2); if (r == lia_move::undef && should_solve_dioph_eq()) r = solve_dioph_eq(); + if (r == lia_move::undef && should_gomory_cut()) r = gomory(lia).get_gomory_cuts(2); if (r == lia_move::undef) r = int_branch(lia)(); if (settings().get_cancel_flag()) r = lia_move::undef; return r; diff --git a/src/math/lp/lar_solver.cpp b/src/math/lp/lar_solver.cpp index ff5ff973e0..9e9d1add14 100644 --- a/src/math/lp/lar_solver.cpp +++ b/src/math/lp/lar_solver.cpp @@ -864,7 +864,7 @@ namespace lp { } lp_status lar_solver::maximize_term(unsigned j, - impq& term_max) { + impq& term_max, bool fix_int_cols) { TRACE(lar_solver, print_values(tout);); SASSERT(get_core_solver().m_r_solver.calc_current_x_is_feasible_include_non_basis()); lar_term term = get_term_to_maximize(j); @@ -879,6 +879,11 @@ namespace lp { return lp_status::UNBOUNDED; } + if (!fix_int_cols) { + set_status(lp_status::OPTIMAL); + return lp_status::OPTIMAL; + } + impq opt_val = term_max; bool change = false; @@ -1120,8 +1125,10 @@ namespace lp { void lar_solver::explain_fixed_column(unsigned j, explanation& ex) { SASSERT(column_is_fixed(j)); - auto* deps = get_bound_constraint_witnesses_for_column(j); - for (auto ci : flatten(deps)) + const column& ul = m_imp->m_columns[j]; + m_imp->m_tmp_dependencies.reset(); + m_imp->m_dependencies.linearize(ul.lower_bound_witness(), ul.upper_bound_witness(), m_imp->m_tmp_dependencies); + for (auto ci : m_imp->m_tmp_dependencies) ex.push_back(ci); } diff --git a/src/math/lp/lar_solver.h b/src/math/lp/lar_solver.h index a5d49dd337..79c729e5d1 100644 --- a/src/math/lp/lar_solver.h +++ b/src/math/lp/lar_solver.h @@ -205,7 +205,9 @@ public: set_column_value(j, v); } - lp_status maximize_term(unsigned j_or_term, impq& term_max); + // fix_int_cols: after maximizing try to move the integer columns to integer values; + // pass false to keep the optimal (possibly fractional) vertex intact, e.g., for the largest cube test + lp_status maximize_term(unsigned j_or_term, impq& term_max, bool fix_int_cols); core_solver_pretty_printer pp(std::ostream& out) const; diff --git a/src/math/lp/lp_params_helper.pyg b/src/math/lp/lp_params_helper.pyg index c3d50ab861..29a10c2d52 100644 --- a/src/math/lp/lp_params_helper.pyg +++ b/src/math/lp/lp_params_helper.pyg @@ -5,9 +5,15 @@ def_module_params(module_name='lp', params=(('dio', BOOL, True, 'use Diophantine equalities'), ('dio_branching_period', UINT, 100, 'Period of calling branching on undef in Diophantine handler'), ('dio_cuts_enable_gomory', BOOL, False, 'enable Gomory cuts together with Diophantine cuts, only relevant when dioph_eq is true'), + ('dio_gomory_enable_period', UINT, 16, 'number of consecutive unproductive (undef) Diophantine-handler calls after which the controller starts running Gomory cuts and the gcd test alongside dio; a dio conflict resets the count and stops them; set very large to never start them this way so Gomory follows dio_cuts_enable_gomory only'), ('dio_cuts_enable_hnf', BOOL, True, 'enable hnf cuts together with Diophantine cuts, only relevant when dioph_eq is true'), ('dio_ignore_big_nums', BOOL, True, 'Ignore the terms with big numbers in the Diophantine handler, only relevant when dioph_eq is true'), ('dio_calls_period', UINT, 1, 'Period of calling the Diophantine handler in the final_check()'), - ('dio_run_gcd', BOOL, False, 'Run the GCD heuristic if dio is on, if dio is disabled the option is not used'), + ('dio_calls_period_decrease', UINT, 2, 'Amount by which dio_calls_period is decreased on each final_check() call where the Diophantine handler is not triggered, until it returns to its initial value'), + ('dio_run_gcd', BOOL, False, 'Run the GCD heuristic if dio is on, if dio is disabled the option is not used'), + ('lcube', BOOL, True, 'use the largest cube test for integer feasibility'), + ('lcube_flips', UINT, 16, 'maximal number of coordinate flips when repairing the rounded largest cube center, only relevant when lcube is true'), + ('int_hammer_period', UINT, 4, 'period (in final_check calls) for the integer cut/cube heuristics (find_cube, hnf, gomory); a smaller value calls them more often'), + ('random_hammers', BOOL, True, 'draw the periodic integer heuristic gates (find_cube, lcube, hnf, gomory, dio) at random with the same 1/period rate instead of a deterministic every-k-th-call modulus'), )) diff --git a/src/math/lp/lp_settings.cpp b/src/math/lp/lp_settings.cpp index 42d6d8ef68..affc299788 100644 --- a/src/math/lp/lp_settings.cpp +++ b/src/math/lp/lp_settings.cpp @@ -37,11 +37,21 @@ void lp::lp_settings::updt_params(params_ref const& _p) { auto eps = p.arith_epsilon(); m_epsilon = rational(std::max(1, (int)(100000*eps)), 100000); m_dio = lp_p.dio(); - m_dio_enable_gomory_cuts = lp_p.dio_cuts_enable_gomory(); + m_dio_cuts_enable_gomory = lp_p.dio_cuts_enable_gomory(); + m_dio_gomory_enable_period = lp_p.dio_gomory_enable_period(); m_dio_enable_hnf_cuts = lp_p.dio_cuts_enable_hnf(); m_dump_bound_lemmas = p.arith_dump_bound_lemmas(); m_dio_ignore_big_nums = lp_p.dio_ignore_big_nums(); m_dio_calls_period = lp_p.dio_calls_period(); + m_dio_calls_period_decrease = lp_p.dio_calls_period_decrease(); m_dio_run_gcd = lp_p.dio_run_gcd(); + m_random_hammers = lp_p.random_hammers(); + m_lcube = lp_p.lcube(); + m_lcube_flips = lp_p.lcube_flips(); + unsigned hammer_period = lp_p.int_hammer_period(); + SASSERT(hammer_period != 0); + m_int_find_cube_period = hammer_period; + m_int_gomory_cut_period = hammer_period; + m_hnf_cut_period = hammer_period; m_max_conflicts = p.max_conflicts(); } diff --git a/src/math/lp/lp_settings.h b/src/math/lp/lp_settings.h index d2e79ea90a..bc1f2044f5 100644 --- a/src/math/lp/lp_settings.h +++ b/src/math/lp/lp_settings.h @@ -112,6 +112,9 @@ struct statistics { unsigned m_gcd_conflicts = 0; unsigned m_cube_calls = 0; unsigned m_cube_success = 0; + unsigned m_lcube_calls = 0; + unsigned m_lcube_success = 0; + unsigned m_lcube_flip_success = 0; unsigned m_patches = 0; unsigned m_patches_success = 0; unsigned m_hnf_cutter_calls = 0; @@ -152,6 +155,9 @@ struct statistics { st.update("arith-gcd-conflict", m_gcd_conflicts); st.update("arith-cube-calls", m_cube_calls); st.update("arith-cube-success", m_cube_success); + st.update("arith-lcube-calls", m_lcube_calls); + st.update("arith-lcube-success", m_lcube_success); + st.update("arith-lcube-flip-success", m_lcube_flip_success); st.update("arith-patches", m_patches); st.update("arith-patches-success", m_patches_success); st.update("arith-hnf-calls", m_hnf_cutter_calls); @@ -252,15 +258,27 @@ private: bool m_print_external_var_name = false; bool m_propagate_eqs = false; bool m_dio = false; - bool m_dio_enable_gomory_cuts = false; + bool m_dio_cuts_enable_gomory = false; + bool m_run_gomory_with_dio = false; + unsigned m_dio_gomory_enable_period = 16; bool m_dio_enable_hnf_cuts = true; bool m_dump_bound_lemmas = false; bool m_dio_ignore_big_nums = false; unsigned m_dio_calls_period = 4; + unsigned m_dio_calls_period_decrease = 2; bool m_dio_run_gcd = true; + bool m_random_hammers = true; + bool m_lcube = true; + unsigned m_lcube_flips = 16; public: + bool lcube() const { return m_lcube; } + unsigned lcube_flips() const { return m_lcube_flips; } unsigned dio_calls_period() const { return m_dio_calls_period; } unsigned & dio_calls_period() { return m_dio_calls_period; } + unsigned dio_calls_period_decrease() const { return m_dio_calls_period_decrease; } + unsigned & dio_calls_period_decrease() { return m_dio_calls_period_decrease; } + bool random_hammers() const { return m_random_hammers; } + bool & random_hammers() { return m_random_hammers; } bool print_external_var_name() const { return m_print_external_var_name; } bool propagate_eqs() const { return m_propagate_eqs;} unsigned hnf_cut_period() const { return m_hnf_cut_period; } @@ -268,8 +286,19 @@ public: unsigned random_next() { return m_rand(); } unsigned random_next(unsigned u ) { return m_rand(u); } bool dio() { return m_dio; } - bool & dio_enable_gomory_cuts() { return m_dio_enable_gomory_cuts; } - bool dio_enable_gomory_cuts() const { return m_dio && m_dio_enable_gomory_cuts; } + // Static config: did the user request Gomory cuts up front? (lp.dio_cuts_enable_gomory) + bool dio_cuts_enable_gomory() const { return m_dio_cuts_enable_gomory; } + // dio_calls_period at which the Diophantine back-off starts running Gomory (lp.dio_gomory_enable_period) + unsigned dio_gomory_enable_period() const { return m_dio_gomory_enable_period; } + // Runtime flag owned by the Diophantine controller, kept separate from the static + // config above so toggling it never clobbers the user's parameter: once dio has + // backed off enough it starts running Gomory cuts alongside dio, and a productive + // dio conflict stops them again. + void start_running_gomory_with_dio() { m_run_gomory_with_dio = true; } + void stop_running_gomory_with_dio() { m_run_gomory_with_dio = false; } + // Effective state read by should_gomory_cut(): allowed if either the user enabled it + // statically or the dio controller started running it, guarded by dio being active. + bool dio_enable_gomory_cuts() const { return m_dio && (m_dio_cuts_enable_gomory || m_run_gomory_with_dio); } bool dio_run_gcd() const { return m_dio && m_dio_run_gcd; } bool dio_enable_hnf_cuts() const { return m_dio && m_dio_enable_hnf_cuts; } bool dio_ignore_big_nums() const { return m_dio_ignore_big_nums; } diff --git a/src/math/lp/nra_solver.cpp b/src/math/lp/nra_solver.cpp index f722019e1f..f5cc61ba9e 100644 --- a/src/math/lp/nra_solver.cpp +++ b/src/math/lp/nra_solver.cpp @@ -969,7 +969,6 @@ lbool solver::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; } diff --git a/src/math/lp/static_matrix.h b/src/math/lp/static_matrix.h index 08db2245d3..c04728cc88 100644 --- a/src/math/lp/static_matrix.h +++ b/src/math/lp/static_matrix.h @@ -60,6 +60,14 @@ std::ostream& operator<<(std::ostream& out, const row_strip& r) { return out << "\n"; } +// Below, static_matrix has a superclass when Z3DEBUG is set, and some +// methods are overrides in that case. +#ifdef Z3DEBUG +#define DEBUG_OVERRIDE override +#else +#define DEBUG_OVERRIDE +#endif + // each assignment for this matrix should be issued only once!!! template class static_matrix @@ -119,9 +127,13 @@ public: void init_empty_matrix(unsigned m, unsigned n); - unsigned row_count() const { return static_cast(m_rows.size()); } + unsigned row_count() const DEBUG_OVERRIDE { + return static_cast(m_rows.size()); + } - unsigned column_count() const { return static_cast(m_columns.size()); } + unsigned column_count() const DEBUG_OVERRIDE { + return static_cast(m_columns.size()); + } unsigned lowest_row_in_column(unsigned col); @@ -197,7 +209,7 @@ public: void cross_out_row_from_column(unsigned col, unsigned k); - T get_elem(unsigned i, unsigned j) const; + T get_elem(unsigned i, unsigned j) const DEBUG_OVERRIDE; unsigned number_of_non_zeroes_in_column(unsigned j) const { return static_cast(m_columns[j].size()); } @@ -218,8 +230,8 @@ public: #ifdef Z3DEBUG unsigned get_number_of_rows() const { return row_count(); } unsigned get_number_of_columns() const { return column_count(); } - virtual void set_number_of_rows(unsigned /*m*/) { } - virtual void set_number_of_columns(unsigned /*n*/) { } + void set_number_of_rows(unsigned /*m*/) override { } + void set_number_of_columns(unsigned /*n*/) override { } #endif T get_balance() const; diff --git a/src/math/polynomial/algebraic_numbers.cpp b/src/math/polynomial/algebraic_numbers.cpp index 07b49e5ce4..a890913c2d 100644 --- a/src/math/polynomial/algebraic_numbers.cpp +++ b/src/math/polynomial/algebraic_numbers.cpp @@ -22,6 +22,7 @@ Notes: #include "util/mpbqi.h" #include "util/timeit.h" #include "util/common_msgs.h" +#include "util/index_sort_with_mutations.h" #include "math/polynomial/algebraic_numbers.h" #include "math/polynomial/upolynomial.h" #include "math/polynomial/sexpr2upolynomial.h" @@ -593,10 +594,57 @@ namespace algebraic_numbers { } } + // Sort an index permutation with a bounds-safe, mutation-aware merge + // sort. The comparator (compare/lt) is NOT pure: it MUTATES the + // algebraic numbers it compares (refining their isolating intervals) and + // may throw on the resource limit, so std::sort would be undefined + // behavior here. See util/index_sort_with_mutations.h for the rationale. + void merge_sort_roots_perm(numeral_vector & r, unsigned_vector & perm) { + unsigned n = perm.size(); + if (n < 2) + return; + unsigned_vector scratch; + scratch.resize(n, 0); + // Strict, total, stable index comparator: decided sign first, then index + // tiebreak (covers the equal/limit case so the order stays deterministic). + auto idx_lt = [&](unsigned x, unsigned y) { + ::sign s = compare(r[x], r[y]); + return s != sign_zero ? s == sign_neg : x < y; + }; + stable_index_merge_sort(perm.data(), scratch.data(), n, idx_lt); + } + void sort_roots(numeral_vector & r) { - if (m_limit.inc()) { - // DEBUG_CODE(check_transitivity(r);); - std::sort(r.begin(), r.end(), lt_proc(m_wrapper)); + if (!m_limit.inc()) + return; + // DEBUG_CODE(check_transitivity(r);); + unsigned n = r.size(); + if (n < 2) + return; + unsigned_vector perm; + perm.resize(n, 0); + for (unsigned i = 0; i < n; ++i) + perm[i] = i; + merge_sort_roots_perm(r, perm); + // Apply the permutation in place via swap cycles. anum swap is a cheap + // pointer swap (move nulls the source), so this is O(n) cheap moves. + unsigned_vector pos; // pos[v] = current position of element v + pos.resize(n, 0); + unsigned_vector at; // at[p] = element currently at position p + at.resize(n, 0); + for (unsigned i = 0; i < n; ++i) { + pos[i] = i; + at[i] = i; + } + for (unsigned target = 0; target < n; ++target) { + unsigned want = perm[target]; // element that should end up at target + unsigned cur = pos[want]; // where it currently is + if (cur == target) + continue; + unsigned other = at[target]; // element currently at target + std::swap(r[target], r[cur]); + at[target] = want; at[cur] = other; + pos[want] = target; pos[other] = cur; } } diff --git a/src/model/datatype_factory.cpp b/src/model/datatype_factory.cpp index b93703acd8..4837bdb900 100644 --- a/src/model/datatype_factory.cpp +++ b/src/model/datatype_factory.cpp @@ -97,7 +97,7 @@ expr * datatype_factory::get_almost_fresh_value(sort * s) { unsigned num = constructor->get_arity(); for (unsigned i = 0; i < num; ++i) { sort * s_arg = constructor->get_domain(i); - if (!found_fresh_arg && (!m_util.is_datatype(s_arg) || !m_util.are_siblings(s, s_arg))) { + if (!found_fresh_arg && (!m_util.is_datatype(s_arg) || !m_util.are_siblings(s, s_arg) || !m_util.is_recursive(s_arg))) { expr * new_arg = m_model.get_fresh_value(s_arg); if (new_arg != nullptr) { found_fresh_arg = true; @@ -105,7 +105,7 @@ expr * datatype_factory::get_almost_fresh_value(sort * s) { continue; } } - if (!found_fresh_arg && m_util.is_datatype(s_arg) && m_util.are_siblings(s, s_arg)) { + if (!found_fresh_arg && m_util.is_datatype(s_arg) && m_util.are_siblings(s, s_arg) && m_util.is_recursive(s_arg)) { recursive = true; expr * last_fresh = get_last_fresh_value(s_arg); args.push_back(last_fresh); diff --git a/src/model/func_interp.cpp b/src/model/func_interp.cpp index 513c39b108..93d70af59c 100644 --- a/src/model/func_interp.cpp +++ b/src/model/func_interp.cpp @@ -231,10 +231,8 @@ void func_interp::insert_new_entry(expr * const * args, expr * r) { m_args_are_values = false; m_entries.push_back(new_entry); if (!m_entry_table && m_entries.size() > 500) { - m_entry_table = alloc(entry_table, 1024, - func_entry_hash(m_arity), func_entry_eq(m_arity)); - for (func_entry* curr : m_entries) - m_entry_table->insert(curr); + init_table(); + ptr_vector null_args; null_args.resize(m_arity, nullptr); m_key = func_entry::mk(m(), m_arity, null_args.data(), nullptr); @@ -243,6 +241,12 @@ void func_interp::insert_new_entry(expr * const * args, expr * r) { m_entry_table->insert(new_entry); } +void func_interp::init_table() { + m_entry_table = alloc(entry_table, 1024, func_entry_hash(m_arity), func_entry_eq(m_arity)); + for (func_entry *curr : m_entries) + m_entry_table->insert(curr); +} + void func_interp::del_entry(unsigned idx) { auto* e = m_entries[idx]; if (m_entry_table) @@ -307,6 +311,12 @@ void func_interp::compress() { if (j < m_entries.size()) { reset_interp_cache(); m_entries.shrink(j); + if (m_entry_table) { + dealloc(m_entry_table); + m_entry_table = nullptr; + if (m_entries.size() > 500) + init_table(); + } } // other compression, if else is a default branch. // or function encode identity. diff --git a/src/model/func_interp.h b/src/model/func_interp.h index e531e01cff..6d64f5168d 100644 --- a/src/model/func_interp.h +++ b/src/model/func_interp.h @@ -40,7 +40,7 @@ class func_entry { // m_result and m_args[i] must be ground terms. expr * m_result; - expr * m_args[]; + expr * m_args[0]; static unsigned get_obj_size(unsigned arity) { return sizeof(func_entry) + arity * sizeof(expr*); } func_entry(ast_manager & m, unsigned arity, expr * const * args, expr * result); @@ -104,6 +104,8 @@ class func_interp { void reset_interp_cache(); + void init_table(); + expr * get_interp_core() const; expr_ref get_array_interp_core(func_decl * f) const; diff --git a/src/model/model_macro_solver.cpp b/src/model/model_macro_solver.cpp index 64881482e6..0e1c449008 100644 --- a/src/model/model_macro_solver.cpp +++ b/src/model/model_macro_solver.cpp @@ -513,7 +513,7 @@ void non_auf_macro_solver::collect_candidates(ptr_vector const& qs, TRACE(model_finder, tout << "considering macro for: " << f->get_name() << "\n"; m->display(tout); tout << "\n";); if (m->is_unconditional() && (!qi->is_auf() || m->get_weight() >= m_mbqi_force_template)) { - full_macros.insert(f, std::make_pair(m, q)); + full_macros.insert(f, {m, q}); cond_macros.erase(f); } else if (!full_macros.contains(f) && !qi->is_auf()) @@ -524,10 +524,8 @@ void non_auf_macro_solver::collect_candidates(ptr_vector const& qs, } void non_auf_macro_solver::process_full_macros(obj_map const& full_macros, obj_hashtable& removed) { - for (auto const& kv : full_macros) { - func_decl* f = kv.m_key; - cond_macro* m = kv.m_value.first; - quantifier* q = kv.m_value.second; + for (auto const &[f, v] : full_macros) { + auto [m, q] = v; SASSERT(m->is_unconditional()); if (add_macro(f, m->get_def())) { get_qinfo(q)->set_the_one(f); diff --git a/src/nlsat/levelwise.cpp b/src/nlsat/levelwise.cpp index e085d6173c..da099d8fdc 100644 --- a/src/nlsat/levelwise.cpp +++ b/src/nlsat/levelwise.cpp @@ -5,6 +5,7 @@ #include "math/polynomial/polynomial.h" #include "nlsat_common.h" #include "util/vector.h" +#include "util/index_sort_with_mutations.h" #include "util/trace.h" #include @@ -956,6 +957,22 @@ namespace nlsat { return m_pm.id(a.ire.p) < m_pm.id(b.ire.p); } + // Sort an index permutation with a bounds-safe, mutation-aware merge + // sort. The comparator (root_function_lt / anum_manager::lt -> compare) + // is NOT pure: it MUTATES the algebraic numbers it compares by refining + // their isolating intervals, and may throw on the resource limit, so a + // single std::sort would be undefined behavior and can crash via an + // out-of-bounds read on timeout. See util/index_sort_with_mutations.h + // for the full rationale. + template + void merge_sort_perm(std_vector& perm, Less less) { + unsigned n = static_cast(perm.size()); + if (n < 2) + return; + std_vector scratch(n); + stable_index_merge_sort(perm.data(), scratch.data(), n, less); + } + // 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) { @@ -982,7 +999,7 @@ namespace nlsat { 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) { + merge_sort_perm(perm, [&](unsigned a, unsigned b) { return root_function_lt(rfs[a], rfs[b], true); }); apply_permutation(rfs, 0, perm); @@ -993,7 +1010,7 @@ namespace nlsat { 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) { + merge_sort_perm(perm, [&](unsigned a, unsigned b) { return root_function_lt(rfs[mid_pos + a], rfs[mid_pos + b], false); }); apply_permutation(rfs, mid_pos, perm); @@ -1192,20 +1209,29 @@ namespace nlsat { if (root_vals.size() < 2) return; - std::sort(root_vals.begin(), root_vals.end(), [&](auto const& a, auto const& b) { - return m_am.lt(a.first, b.first); + // Sort root values by an index permutation with the bounds-safe, + // mutation-aware merge sort (see merge_sort_perm). As in + // sort_root_function_partitions, the comparator (anum_manager::lt -> + // compare) MUTATES the algebraic numbers it compares (it refines their + // isolating intervals and may hit the resource limit and throw), so it is + // not a fixed strict weak ordering over a single sort; std::sort here would + // be undefined behavior and crash via an out-of-bounds read on timeout. + std_vector perm(root_vals.size()); + std::iota(perm.begin(), perm.end(), 0u); + merge_sort_perm(perm, [&](unsigned a, unsigned b) { + return m_am.lt(root_vals[a].first, root_vals[b].first); }); - + TRACE(lws, tout << " Sorted roots:\n"; - for (unsigned j = 0; j < root_vals.size(); ++j) - m_pm.display(m_am.display_decimal(tout << " [" << j << "] val=", root_vals[j].first, 5) << " poly=", root_vals[j].second) << "\n"; + for (unsigned j = 0; j < perm.size(); ++j) + m_pm.display(m_am.display_decimal(tout << " [" << j << "] val=", root_vals[perm[j]].first, 5) << " poly=", root_vals[perm[j]].second) << "\n"; ); std::set> added_pairs; - for (unsigned j = 0; j + 1 < root_vals.size(); ++j) { - poly* p1 = root_vals[j].second; - poly* p2 = root_vals[j + 1].second; + for (unsigned j = 0; j + 1 < perm.size(); ++j) { + poly* p1 = root_vals[perm[j]].second; + poly* p2 = root_vals[perm[j + 1]].second; if (!p1 || !p2 || p1 == p2) continue; if (p1 > p2) std::swap(p1, p2); diff --git a/src/nlsat/nlsat_interval_set.cpp b/src/nlsat/nlsat_interval_set.cpp index dc5740e103..160c8afb66 100644 --- a/src/nlsat/nlsat_interval_set.cpp +++ b/src/nlsat/nlsat_interval_set.cpp @@ -261,7 +261,7 @@ namespace nlsat { new_set->m_full = full; new_set->m_ref_count = 0; new_set->m_num_intervals = sz; - memcpy(new_set->m_intervals, buf.data(), sizeof(interval)*sz); + memcpy(static_cast(new_set->m_intervals), static_cast(buf.data()), sizeof(interval)*sz); return new_set; } diff --git a/src/params/CMakeLists.txt b/src/params/CMakeLists.txt index 732430fe35..9aea5b9187 100644 --- a/src/params/CMakeLists.txt +++ b/src/params/CMakeLists.txt @@ -28,7 +28,6 @@ z3_add_component(params seq_rewriter_params.pyg sls_params.pyg smt_params_helper.pyg - smt_parallel_params.pyg solver_params.pyg tactic_params.pyg EXTRA_REGISTER_MODULE_HEADERS diff --git a/src/params/smt_parallel_params.pyg b/src/params/smt_parallel_params.pyg deleted file mode 100644 index dde7656ffc..0000000000 --- a/src/params/smt_parallel_params.pyg +++ /dev/null @@ -1,12 +0,0 @@ -def_module_params('smt_parallel', - export=True, - description='Experimental parameters for parallel solving', - 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'), - ('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/qe/lite/qe_lite_tactic.cpp b/src/qe/lite/qe_lite_tactic.cpp index 4f234a58e7..774c619cfc 100644 --- a/src/qe/lite/qe_lite_tactic.cpp +++ b/src/qe/lite/qe_lite_tactic.cpp @@ -2467,6 +2467,7 @@ private: cur_vals[i] = var_lbs[i]; var_subst vs(m, false); + inv_var_shifter shift(m); expr_ref_vector disjuncts(m); while (true) { @@ -2479,6 +2480,7 @@ private: for (expr* p : payload) { expr_ref inst(m); inst = vs(p, subst_map.size(), subst_map.data()); + shift(inst, num_decls, inst); inst_conjs.push_back(inst); } expr_ref inst_body(m); diff --git a/src/qe/qsat.cpp b/src/qe/qsat.cpp index 6ab3add0d1..86234962c0 100644 --- a/src/qe/qsat.cpp +++ b/src/qe/qsat.cpp @@ -1290,6 +1290,15 @@ namespace qe { TRACE(qe, tout << fml << "\n";); + // qe/qe2 over a quantifier-free formula has nothing to eliminate. + // Under check-sat semantics the free variables are implicitly + // existentially quantified, so decide satisfiability directly + // instead of leaving an undecided residual goal (which would be + // reported as 'unknown'). + flet _mode(m_mode, + (m_mode == qsat_qe || m_mode == qsat_qe_rec) && !has_quantifiers(fml) + ? qsat_sat : m_mode); + if (m_mode == qsat_qe_rec) { fml = elim_rec(fml); in->reset(); diff --git a/src/sat/sat_solver.cpp b/src/sat/sat_solver.cpp index 09f874e789..0a109bd0c5 100644 --- a/src/sat/sat_solver.cpp +++ b/src/sat/sat_solver.cpp @@ -132,6 +132,8 @@ namespace sat { m_best_phase.reset(); m_phase.reset(); m_prev_phase.reset(); + m_phase_birthdate.reset(); + m_best_phase_birthdate.reset(); m_assigned_since_gc.reset(); m_last_conflict.reset(); m_last_propagation.reset(); @@ -161,6 +163,8 @@ namespace sat { m_phase[v] = src.m_phase[v]; m_best_phase[v] = src.m_best_phase[v]; m_prev_phase[v] = src.m_prev_phase[v]; + m_phase_birthdate[v] = src.m_phase_birthdate[v]; + m_best_phase_birthdate[v] = src.m_best_phase_birthdate[v]; // inherit activity: m_activity[v] = src.m_activity[v]; @@ -267,6 +271,8 @@ namespace sat { m_phase[v] = false; m_best_phase[v] = false; m_prev_phase[v] = false; + m_phase_birthdate[v] = 0; + m_best_phase_birthdate[v] = 0; m_assigned_since_gc[v] = false; m_last_conflict[v] = 0; m_last_propagation[v] = 0; @@ -308,6 +314,8 @@ namespace sat { m_phase.push_back(false); m_best_phase.push_back(false); m_prev_phase.push_back(false); + m_phase_birthdate.push_back(0); + m_best_phase_birthdate.push_back(0); m_assigned_since_gc.push_back(false); m_last_conflict.push_back(0); m_last_propagation.push_back(0); @@ -645,6 +653,26 @@ namespace sat { return 3*cls_allocator().get_allocation_size()/2 + memory::get_allocation_size() > memory::get_max_memory_size(); } + void solver::set_phase(literal l) { + if (l.var() >= num_vars()) + return; + bool value = !l.sign(); + set_phase(l.var(), value); + set_best_phase(l.var(), value); + } + + void solver::set_phase(bool_var v, bool value) { + if (m_phase[v] != value) + m_phase_birthdate[v] = m_stats.m_conflicts; + m_phase[v] = value; + } + + void solver::set_best_phase(bool_var v, bool value) { + if (m_best_phase[v] != value) + m_best_phase_birthdate[v] = m_stats.m_conflicts; + m_best_phase[v] = value; + } + struct solver::cmp_activity { solver& s; cmp_activity(solver& s):s(s) {} @@ -896,7 +924,7 @@ namespace sat { m_assignment[(~l).index()] = l_false; bool_var v = l.var(); m_justification[v] = j; - m_phase[v] = !l.sign(); + set_phase(v, !l.sign()); m_assigned_since_gc[v] = true; m_trail.push_back(l); @@ -904,17 +932,17 @@ namespace sat { case BH_VSIDS: break; case BH_CHB: - m_last_propagation[v] = m_stats.m_conflict; + m_last_propagation[v] = m_stats.m_conflicts; break; } if (m_config.m_anti_exploration) { - uint64_t age = m_stats.m_conflict - m_canceled[v]; + uint64_t age = m_stats.m_conflicts - m_canceled[v]; if (age > 0) { double decay = pow(0.95, static_cast(age)); set_activity(v, static_cast(m_activity[v] * decay)); // NB. MapleSAT does not update canceled. - m_canceled[v] = m_stats.m_conflict; + m_canceled[v] = m_stats.m_conflicts; } } @@ -1378,8 +1406,10 @@ namespace sat { lbool r = m_local_search->check(_lits.size(), _lits.data(), nullptr); auto const& mdl = m_local_search->get_model(); if (mdl.size() == m_best_phase.size()) { - for (unsigned i = 0; i < m_best_phase.size(); ++i) - m_best_phase[i] = l_true == mdl[i]; + for (unsigned i = 0; i < m_best_phase.size(); ++i) { + bool is_true = l_true == mdl[i]; + set_best_phase(i, is_true); + } if (r == l_true) { m_conflicts_since_restart = 0; @@ -1671,12 +1701,12 @@ namespace sat { while (!m_case_split_queue.empty()) { if (m_config.m_anti_exploration) { next = m_case_split_queue.min_var(); - auto age = m_stats.m_conflict - m_canceled[next]; + auto age = m_stats.m_conflicts - m_canceled[next]; while (age > 0) { set_activity(next, static_cast(m_activity[next] * pow(0.95, static_cast(age)))); - m_canceled[next] = m_stats.m_conflict; + m_canceled[next] = m_stats.m_conflicts; next = m_case_split_queue.min_var(); - age = m_stats.m_conflict - m_canceled[next]; + age = m_stats.m_conflicts - m_canceled[next]; } } next = m_case_split_queue.next_var(); @@ -1714,6 +1744,25 @@ namespace sat { } } + void solver::get_backbone_candidates(literal_vector& lits, unsigned max_num) { + struct candidate { + literal lit; + uint64_t age; + }; + svector cands; + uint64_t now = m_stats.m_conflicts; + for (bool_var v = 0; v < num_vars(); ++v) { + if (value(v) != l_undef || was_eliminated(v)) + continue; + bool is_pos = guess(v); + cands.push_back({ literal(v, !is_pos), now - get_phase_birthdate(v) }); + } + std::stable_sort(cands.begin(), cands.end(), + [](candidate const& a, candidate const& b) { return a.age > b.age; }); + for (unsigned i = 0; i < cands.size() && i < max_num; ++i) + lits.push_back(cands[i].lit); + } + bool solver::decide() { bool_var next; lbool phase = l_undef; @@ -2145,8 +2194,9 @@ namespace sat { for (bool_var v = 0; v < num; ++v) { if (!was_eliminated(v)) { m_model[v] = value(v); - m_phase[v] = value(v) == l_true; - m_best_phase[v] = value(v) == l_true; + bool is_true = value(v) == l_true; + set_phase(v, is_true); + set_best_phase(v, is_true); } } TRACE(sat_mc_bug, m_mc.display(tout);); @@ -2274,7 +2324,7 @@ namespace sat { m_restart_logs++; std::stringstream strm; - strm << "(sat.stats " << std::setw(6) << m_stats.m_conflict << " " + strm << "(sat.stats " << std::setw(6) << m_stats.m_conflicts << " " << std::setw(6) << m_stats.m_decision << " " << std::setw(4) << m_stats.m_restart << mk_stat(*this) @@ -2432,7 +2482,7 @@ namespace sat { m_conflicts_since_init++; m_conflicts_since_restart++; m_conflicts_since_gc++; - m_stats.m_conflict++; + m_stats.m_conflicts++; if (m_step_size > m_config.m_step_size_min) m_step_size -= m_config.m_step_size_dec; @@ -2564,7 +2614,7 @@ namespace sat { tout << "missed " << lit << "@" << lvl(lit) << "\n";); CTRACE(sat, idx == 0, display(tout);); if (idx == 0) - IF_VERBOSE(0, verbose_stream() << "num-conflicts: " << m_stats.m_conflict << "\n"); + IF_VERBOSE(0, verbose_stream() << "num-conflicts: " << m_stats.m_conflicts << "\n"); VERIFY(idx > 0); idx--; } @@ -2874,7 +2924,7 @@ namespace sat { inc_activity(var); break; case BH_CHB: - m_last_conflict[var] = m_stats.m_conflict; + m_last_conflict[var] = m_stats.m_conflicts; break; default: break; @@ -2915,14 +2965,15 @@ namespace sat { for (unsigned i = head; i < sz; ++i) { bool_var v = m_trail[i].var(); TRACE(forget_phase, tout << "forgetting phase of v" << v << "\n";); - m_phase[v] = m_rand() % 2 == 0; + bool value = m_rand() % 2 == 0; + set_phase(v, value); } if (is_sat_phase() && head >= m_best_phase_size) { m_best_phase_size = head; IF_VERBOSE(12, verbose_stream() << "sticky trail: " << head << "\n"); for (unsigned i = 0; i < head; ++i) { bool_var v = m_trail[i].var(); - m_best_phase[v] = m_phase[v]; + set_best_phase(v, m_phase[v]); } set_has_new_best_phase(true); } @@ -2971,23 +3022,30 @@ namespace sat { void solver::do_rephase() { switch (m_config.m_phase) { case PS_ALWAYS_TRUE: - for (auto& p : m_phase) p = true; + for (unsigned i = 0; i < m_phase.size(); ++i) + set_phase(i, true); break; case PS_ALWAYS_FALSE: - for (auto& p : m_phase) p = false; + for (unsigned i = 0; i < m_phase.size(); ++i) + set_phase(i, false); break; case PS_FROZEN: break; case PS_BASIC_CACHING: switch (m_rephase.count % 4) { case 0: - for (auto& p : m_phase) p = (m_rand() % 2) == 0; + for (unsigned i = 0; i < m_phase.size(); ++i) { + bool value = (m_rand() % 2) == 0; + set_phase(i, value); + } break; case 1: - for (auto& p : m_phase) p = false; + for (unsigned i = 0; i < m_phase.size(); ++i) + set_phase(i, false); break; case 2: - for (auto& p : m_phase) p = !p; + for (unsigned i = 0; i < m_phase.size(); ++i) + set_phase(i, !m_phase[i]); break; default: break; @@ -2995,18 +3053,21 @@ namespace sat { break; case PS_SAT_CACHING: if (m_search_state == s_sat) - for (unsigned i = 0; i < m_phase.size(); ++i) - m_phase[i] = m_best_phase[i]; + for (unsigned i = 0; i < m_phase.size(); ++i) + set_phase(i, m_best_phase[i]); break; case PS_RANDOM: - for (auto& p : m_phase) p = (m_rand() % 2) == 0; + for (unsigned i = 0; i < m_phase.size(); ++i) { + bool value = (m_rand() % 2) == 0; + set_phase(i, value); + } break; case PS_LOCAL_SEARCH: if (m_search_state == s_sat) { if (m_rand() % 2 == 0) bounded_local_search(); - for (unsigned i = 0; i < m_phase.size(); ++i) - m_phase[i] = m_best_phase[i]; + for (unsigned i = 0; i < m_phase.size(); ++i) + set_phase(i, m_best_phase[i]); } break; @@ -3601,6 +3662,8 @@ namespace sat { m_phase.shrink(v); m_best_phase.shrink(v); m_prev_phase.shrink(v); + m_phase_birthdate.shrink(v); + m_best_phase_birthdate.shrink(v); m_assigned_since_gc.shrink(v); m_simplifier.reset_todos(); } @@ -3644,7 +3707,7 @@ namespace sat { SASSERT(value(v) == l_undef); m_case_split_queue.unassign_var_eh(v); if (m_config.m_anti_exploration) { - m_canceled[v] = m_stats.m_conflict; + m_canceled[v] = m_stats.m_conflicts; } } m_trail.shrink(old_sz); @@ -3812,7 +3875,7 @@ namespace sat { double multiplier = m_config.m_reward_offset * (is_sat ? m_config.m_reward_multiplier : 1.0); for (unsigned i = qhead; i < m_trail.size(); ++i) { auto v = m_trail[i].var(); - auto d = m_stats.m_conflict - m_last_conflict[v] + 1; + auto d = m_stats.m_conflicts - m_last_conflict[v] + 1; if (d == 0) d = 1; auto reward = multiplier / d; auto activity = m_activity[v]; @@ -4745,7 +4808,7 @@ namespace sat { st.update("sat mk var", m_mk_var); st.update("sat gc clause", m_gc_clause); st.update("sat del clause", m_del_clause); - st.update("sat conflicts", m_conflict); + st.update("sat conflicts", m_conflicts); st.update("sat decisions", m_decision); st.update("sat propagations 2ary", m_bin_propagate); st.update("sat propagations 3ary", m_ter_propagate); diff --git a/src/sat/sat_solver.h b/src/sat/sat_solver.h index 9aa00ae47c..66352387a2 100644 --- a/src/sat/sat_solver.h +++ b/src/sat/sat_solver.h @@ -60,7 +60,7 @@ namespace sat { unsigned m_mk_bin_clause; unsigned m_mk_ter_clause; unsigned m_mk_clause; - unsigned m_conflict; + unsigned m_conflicts; unsigned m_propagate; unsigned m_bin_propagate; unsigned m_ter_propagate; @@ -148,6 +148,8 @@ namespace sat { bool_vector m_phase; bool_vector m_best_phase; bool_vector m_prev_phase; + svector m_phase_birthdate; + svector m_best_phase_birthdate; bool m_new_best_phase = false; svector m_assigned_since_gc; search_state m_search_state; @@ -373,12 +375,18 @@ namespace sat { bool was_eliminated(bool_var v) const { return m_eliminated[v]; } void set_eliminated(bool_var v, bool f) override; bool was_eliminated(literal l) const { return was_eliminated(l.var()); } - void set_phase(literal l) override { if (l.var() < num_vars()) m_best_phase[l.var()] = m_phase[l.var()] = !l.sign(); } + void set_phase(literal l) override; + void set_phase(bool_var v, bool value); + void set_best_phase(bool_var v, bool value); bool get_phase(bool_var b) { return m_phase.get(b, false); } bool get_best_phase(bool_var b) { return m_best_phase.get(b, false); } + uint64_t get_phase_birthdate(bool_var b) const { return m_phase_birthdate.get(b, 0); } + uint64_t get_best_phase_birthdate(bool_var b) const { return m_best_phase_birthdate.get(b, 0); } void set_has_new_best_phase(bool b) { m_new_best_phase = b; } bool has_new_best_phase() const { return m_new_best_phase; } void move_to_front(bool_var b); + unsigned get_activity(bool_var v) const { return m_activity[v]; } + void get_backbone_candidates(literal_vector& lits, unsigned max_num); unsigned scope_lvl() const { return m_scope_lvl; } unsigned search_lvl() const { return m_search_lvl; } bool at_search_lvl() const { return m_scope_lvl == m_search_lvl; } @@ -440,6 +448,8 @@ namespace sat { void set_par(parallel* p, unsigned id); bool canceled() { return !m_rlimit.inc(); } config const& get_config() const { return m_config; } + void set_max_conflicts(unsigned n) { m_config.m_max_conflicts = n; } + unsigned get_max_conflicts() const { return m_config.m_max_conflicts; } void set_drat(bool d) { m_config.m_drat = d; } drat& get_drat() { return m_drat; } drat* get_drat_ptr() { return &m_drat; } diff --git a/src/sat/sat_solver/inc_sat_solver.cpp b/src/sat/sat_solver/inc_sat_solver.cpp index 2a98bc3aed..c129841932 100644 --- a/src/sat/sat_solver/inc_sat_solver.cpp +++ b/src/sat/sat_solver/inc_sat_solver.cpp @@ -27,7 +27,6 @@ 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" @@ -391,6 +390,15 @@ public: if (m_preprocess) m_preprocess->collect_statistics(st); m_solver.collect_statistics(st); } + + void set_max_conflicts(unsigned max_conflicts) override { + m_solver.set_max_conflicts(max_conflicts); + } + + unsigned get_max_conflicts() const override { + return m_solver.get_max_conflicts(); + } + void get_unsat_core(expr_ref_vector & r) override { r.reset(); r.append(m_core.size(), m_core.data()); @@ -405,6 +413,46 @@ public: } } + unsigned get_assign_level(expr* e) const override { + m.is_not(e, e); + sat::bool_var bv = m_map.to_bool_var(e); + return bv == sat::null_bool_var ? UINT_MAX : m_solver.lvl(bv); + } + + bool is_relevant(expr* e) const override { + m.is_not(e, e); + sat::bool_var bv = m_map.to_bool_var(e); + if (bv == sat::null_bool_var) + return true; + auto* ext = dynamic_cast(m_solver.get_extension()); + return !ext || ext->is_relevant(bv); + } + + unsigned get_num_bool_vars() const override { + return m_solver.num_vars(); + } + + sat::bool_var get_bool_var(expr* e) const override { + m.is_not(e, e); + return m_map.to_bool_var(e); + } + + expr* bool_var2expr(sat::bool_var v) const override { + return v < m_solver.num_vars() ? m_map.bool_var2expr(v) : nullptr; + } + + lbool get_assignment(sat::bool_var v) const override { + return v < m_solver.num_vars() ? m_solver.value(v) : l_undef; + } + + double get_activity(sat::bool_var v) const override { + return v < m_solver.num_vars() ? static_cast(m_solver.get_activity(v)) : 0.0; + } + + bool was_eliminated(sat::bool_var v) const override { + return v < m_solver.num_vars() && m_solver.was_eliminated(v); + } + expr_ref_vector get_trail(unsigned max_level) override { expr_ref_vector result(m); unsigned sz = m_solver.trail_size(); @@ -482,6 +530,70 @@ public: return fmls; } + expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { + if (!is_internalized()) { + lbool r = internalize_formulas(); + if (r != l_true) + return expr_ref(m); + } + convert_internalized(); + if (m_solver.inconsistent()) + return expr_ref(m); + + obj_hashtable invalid_split_atoms_set; + for (expr* e : invalid_split_atoms) { + expr* atom = e; + m.is_not(e, atom); + invalid_split_atoms_set.insert(atom); + } + + expr_ref result(m); + double score = 0.0; + unsigned n = 0; + unsigned search_lvl = m_solver.search_lvl(); + for (auto& kv : m_map) { + sat::bool_var v = kv.m_value; + if (was_eliminated(v)) + continue; + if (get_assignment(v) != l_undef && m_solver.lvl(v) <= search_lvl) + continue; + expr* e = kv.m_key; + if (!e) + continue; + expr* atom = e; + m.is_not(e, atom); + if (invalid_split_atoms_set.contains(atom)) + continue; + double new_score = get_activity(v); + if (new_score > score || !result || (new_score == score && m_solver.rand()(++n) == 0)) { + score = new_score; + result = e; + } + } + return result; + } + + void get_backbone_candidates(vector& candidates, unsigned max_num) override { + if (!is_internalized()) { + lbool r = internalize_formulas(); + if (r != l_true) + return; + } + convert_internalized(); + sat::literal_vector lits; + m_solver.get_backbone_candidates(lits, max_num); + expr_ref_vector lit2expr(m); + lit2expr.resize(m_solver.num_vars() * 2); + m_map.mk_inv(lit2expr); + uint64_t now = m_solver.get_stats().m_conflicts; + for (sat::literal lit : lits) { + expr* e = lit2expr.get(lit.index()); + if (!e) + continue; + candidates.push_back(scored_literal(m, e, static_cast(now - m_solver.get_phase_birthdate(lit.var())))); + } + } + expr* congruence_next(expr* e) override { return e; } expr* congruence_root(expr* e) override { return e; } expr_ref congruence_explain(expr* a, expr* b) override { return expr_ref(m.mk_eq(a, b), m); } @@ -1186,7 +1298,5 @@ tactic * mk_psat_tactic(ast_manager& m, params_ref const& p) { parallel_params pp(p); 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_solver.cpp b/src/sat/smt/array_solver.cpp index dca1b3f51f..d955a29d0d 100644 --- a/src/sat/smt/array_solver.cpp +++ b/src/sat/smt/array_solver.cpp @@ -203,6 +203,8 @@ namespace array { ctx.push_vec(d.m_parent_lambdas, lambda); if (should_prop_upward(d)) propagate_select_axioms(d, lambda); + if (d.m_has_default) + push_axiom(default_axiom(lambda)); } void solver::add_parent_default(theory_var v) { diff --git a/src/sat/smt/atom2bool_var.cpp b/src/sat/smt/atom2bool_var.cpp index 68a263c856..b6760c9439 100644 --- a/src/sat/smt/atom2bool_var.cpp +++ b/src/sat/smt/atom2bool_var.cpp @@ -49,6 +49,13 @@ sat::bool_var atom2bool_var::to_bool_var(expr * n) const { return m_mapping[idx].m_value; } +expr* atom2bool_var::bool_var2expr(sat::bool_var v) const { + for (auto const& kv : m_mapping) + if (kv.m_value == v) + return kv.m_key; + return nullptr; +} + struct collect_boolean_interface_proc { struct visitor { obj_hashtable & m_r; diff --git a/src/sat/smt/atom2bool_var.h b/src/sat/smt/atom2bool_var.h index 794429701d..cee7a98829 100644 --- a/src/sat/smt/atom2bool_var.h +++ b/src/sat/smt/atom2bool_var.h @@ -29,6 +29,7 @@ public: atom2bool_var(ast_manager & m):expr2var(m) {} void insert(expr * n, sat::bool_var v) { expr2var::insert(n, v); } sat::bool_var to_bool_var(expr * n) const; + expr* bool_var2expr(sat::bool_var v) const; void mk_inv(expr_ref_vector & lit2expr) const; void mk_var_inv(expr_ref_vector & var2expr) const; // return true if the mapping contains uninterpreted atoms. diff --git a/src/sat/smt/bv_ackerman.cpp b/src/sat/smt/bv_ackerman.cpp index a709c16f3a..1bb84bc099 100644 --- a/src/sat/smt/bv_ackerman.cpp +++ b/src/sat/smt/bv_ackerman.cpp @@ -155,7 +155,7 @@ namespace bv { void ackerman::propagate() { auto* n = m_queue; vv* k = nullptr; - unsigned num_prop = static_cast(s.s().get_stats().m_conflict * s.get_config().m_dack_factor); + unsigned num_prop = static_cast(s.s().get_stats().m_conflicts * s.get_config().m_dack_factor); num_prop = std::min(num_prop, m_table.size()); for (unsigned i = 0; i < num_prop; ++i, n = k) { k = n->next(); diff --git a/src/sat/smt/euf_ackerman.cpp b/src/sat/smt/euf_ackerman.cpp index 67a98b2f63..568df60342 100644 --- a/src/sat/smt/euf_ackerman.cpp +++ b/src/sat/smt/euf_ackerman.cpp @@ -171,7 +171,7 @@ namespace euf { SASSERT(ctx.s().at_base_lvl()); auto* n = m_queue; inference* k = nullptr; - unsigned num_prop = static_cast(ctx.s().get_stats().m_conflict * ctx.m_config.m_dack_factor); + unsigned num_prop = static_cast(ctx.s().get_stats().m_conflicts * ctx.m_config.m_dack_factor); num_prop = std::min(num_prop, m_table.size()); for (unsigned i = 0; i < num_prop; ++i, n = k) { k = n->next(); diff --git a/src/smt/seq/seq_nielsen.cpp b/src/smt/seq/seq_nielsen.cpp index 3f168ee9ee..6ae072c013 100644 --- a/src/smt/seq/seq_nielsen.cpp +++ b/src/smt/seq/seq_nielsen.cpp @@ -5097,7 +5097,7 @@ namespace seq { svector& mem_literals) const { SASSERT(m_root); const auto deps = collect_conflict_deps(); - vector vs; + vector vs; m_dep_mgr.linearize(deps, vs); for (dep_source const& d : vs) { if (std::holds_alternative(d)) diff --git a/src/smt/seq_eq_solver.cpp b/src/smt/seq_eq_solver.cpp index e368e137bd..03cdd6c703 100644 --- a/src/smt/seq_eq_solver.cpp +++ b/src/smt/seq_eq_solver.cpp @@ -189,7 +189,7 @@ bool theory_seq::len_based_split(depeq const& e) { expr_ref_vector const& rs = e.rs; int offset = 0; - if (!has_len_offset(ls, rs, offset)) + if (!has_len_offset(ls, rs, offset) || offset == 0) return false; TRACE(seq, tout << "split based on length\n";); diff --git a/src/smt/seq_model.cpp b/src/smt/seq_model.cpp index c1389561e7..b27285f0eb 100644 --- a/src/smt/seq_model.cpp +++ b/src/smt/seq_model.cpp @@ -470,9 +470,10 @@ namespace smt { re_expr = m_seq.re.mk_inter(re_expr, loop); } + zstring str; expr_ref witness(m); // We checked non-emptiness during Nielsen already - lbool wr = m_rewriter.some_seq_in_re(re_expr, witness); + lbool wr = m_rewriter.some_string_in_re(re_expr, str); if (wr != l_true) { // some_seq_in_re can fail (l_undef / l_false) on regexes it does // not fully support — notably projection operators (re.proj), @@ -482,7 +483,7 @@ namespace smt { wr = derivative_witness(m_sg.mk(re_expr), witness); } if (wr == l_true) { - SASSERT(witness); + witness = m_seq.str.mk_string(str); m_trail.push_back(witness); m_factory->register_value(witness); return witness; diff --git a/src/smt/seq_regex.cpp b/src/smt/seq_regex.cpp index 4b294683ab..e0423599dc 100644 --- a/src/smt/seq_regex.cpp +++ b/src/smt/seq_regex.cpp @@ -248,6 +248,17 @@ namespace smt { th.add_axiom(~lit); return true; } + // Fall back to antimirov NFA reachability. The lazy state graph + // keys states by AST identity and cannot close on intersections / + // complements whose derivative product states do not canonicalize, + // so it never detects their emptiness. re_is_empty decides + // emptiness directly (the same procedure propagate_eq already uses + // for re.none equalities). + if (re_is_empty(r) == l_true) { + STRACE(seq_regex_brief, tout << "(empty:re) ";); + th.add_axiom(~lit); + return true; + } } return false; } @@ -413,8 +424,15 @@ namespace smt { } expr_ref seq_regex::symmetric_diff(expr* r1, expr* r2) { - seq_rewriter rw(m); - auto r = rw.mk_symmetric_diff(r1, r2); + expr_ref r(m); + if (r1 == r2) + r = re().mk_empty(r1->get_sort()); + else if (re().is_empty(r1)) + r = r2; + else if (re().is_empty(r2)) + r = r1; + else + r = re().mk_union(re().mk_diff(r1, r2), re().mk_diff(r2, r1)); rewrite(r); return r; } @@ -478,6 +496,24 @@ namespace smt { if (re().is_empty(r)) //trivially true return; + // When one side is re.none the equation is a pure emptiness check on + // the other regex (symmetric_diff already returned it as r). Decide + // it directly by antimirov NFA reachability instead of running the + // bisimulation/XOR closure, which would build large un-canonicalized + // product states for intersections of contains-patterns. + if ((re().is_empty(r1) || re().is_empty(r2)) && is_ground(r)) { + switch (re_is_empty(r)) { + case l_true: + STRACE(seq_regex_brief, tout << "empty:eq ";); + return; // languages equal (both empty): trivially true + case l_false: + STRACE(seq_regex_brief, tout << "empty:neq ";); + th.add_axiom(~th.mk_eq(r1, r2, false), false_literal); + return; + case l_undef: + break; + } + } // Try the bisimulation procedure on ground regexes first. If it // returns a definite answer, dispatch the corresponding axiom and // bypass the symbolic emptiness/derivative closure. @@ -579,16 +615,16 @@ namespace smt { lits.push_back(null_lit); expr_ref_pair_vector cofactors(m); - get_cofactors(d, cofactors); - for (auto const& p : cofactors) { - if (is_member(p.second, u)) + seq_rw().get_cofactors(hd, d, cofactors); + for (auto const& [c, r] : cofactors) { + if (is_member(r, u)) continue; - expr_ref cond(p.first, m); + expr_ref cond(c, m); seq_rw().elim_condition(hd, cond); rewrite(cond); if (m.is_false(cond)) continue; - expr_ref next_non_empty = sk().mk_is_non_empty(p.second, re().mk_union(u, p.second), n); + expr_ref next_non_empty = sk().mk_is_non_empty(r, re().mk_union(u, r), n); if (!m.is_true(cond)) next_non_empty = m.mk_and(cond, next_non_empty); lits.push_back(th.mk_literal(next_non_empty)); @@ -689,40 +725,113 @@ namespace smt { } /* - Return a list of all target regexes in the derivative of a regex r, - ignoring the conditions along each path. + Decide emptiness of a ground regex r via antimirov-mode NFA + reachability. - The derivative construction uses (:var 0) and tries - to eliminate unsat condition paths but it does not perform - full satisfiability checks and it is not guaranteed - that all targets are actually reachable + The symbolic derivative engine runs in antimirov mode, so the + derivative of an intersection distributes into a *set* of individual + product states inter(A_i, B_j) (each a small, ground regex) rather + than one giant union-of-intersections term. get_derivative_targets + enumerates these NFA successor states. + + We short-circuit to l_false (non-empty) as soon as a reachable state + is nullable (accepts the empty word) or classical (a regex built only + from to_re/all/union/concat/star/plus/opt/loop, hence non-empty). An + intersection itself is never classical, but once one operand reduces + to Σ* the intersection collapses (via the derivative's subset + simplification) to the other, classical, operand. + + If the worklist is exhausted with no such state, r is empty (l_true). + Returns l_undef if a step bound is hit, so callers can fall back to + the general procedure. */ - void seq_regex::get_derivative_targets(expr* r, expr_ref_vector& targets) { - // constructs the derivative wrt (:var 0) - expr_ref d(seq_rw().mk_derivative(r), m); - - // use DFS to collect all the targets (leaf regexes) in d. - expr* _1 = nullptr, * e1 = nullptr, * e2 = nullptr; - obj_hashtable::entry* _2 = nullptr; - vector workset; - workset.push_back(d); - obj_hashtable done; - done.insert(d); - while (workset.size() > 0) { - expr* e = workset.back(); - workset.pop_back(); - if (m.is_ite(e, _1, e1, e2) || re().is_union(e, e1, e2)) { - if (done.insert_if_not_there_core(e1, _2)) - workset.push_back(e1); - if (done.insert_if_not_there_core(e2, _2)) - workset.push_back(e2); + lbool seq_regex::re_is_empty(expr* r) { + if (re().is_empty(r)) + return l_true; + expr_ref_vector pinned(m); + obj_hashtable visited; + ptr_vector work; + work.push_back(r); + visited.insert(r); + pinned.push_back(r); + unsigned const bound = 100000; + unsigned steps = 0; + while (!work.empty()) { + if (++steps > bound) + return l_undef; + expr* s = work.back(); + work.pop_back(); + auto info = re().get_info(s); + if (!info.is_known()) + return l_undef; + // ε ∈ L(s) or s is a non-empty classical regex ⇒ L(r) non-empty. + if (info.nullable == l_true || info.classical) + return l_false; + // Dead state: prune (min_length == UINT_MAX means no word is + // accepted from here). + if (info.min_length == UINT_MAX) + continue; + expr_ref_vector targets(m); + get_derivative_targets(s, targets); + for (expr* t : targets) { + if (visited.contains(t)) + continue; + visited.insert(t); + pinned.push_back(t); + work.push_back(t); } - else if (!re().is_empty(e)) - targets.push_back(e); } + return l_true; } + /* + Return a list of all reachable target regexes in the derivative of a + regex r. + + The derivative is taken wrt (:var 0) and its reachable leaves are + enumerated with the path-aware cofactor engine, which conjoins the + ITE-path conditions and prunes infeasible character-range combinations + (e.g. a nested branch requiring elem = 'a' and elem = 'B'). Each leaf + is re-normalized with the path-aware smart constructors so that + semantically equal states stay syntactically identical (essential for + state dedup in the emptiness closure). + + Without this pruning the naive ITE-tree DFS would reach infeasible + leaves; an infeasible classical (intersection/complement-free) leaf + would then be misjudged as a non-empty residual. + */ + void seq_regex::get_derivative_targets(expr* r, expr_ref_vector& targets) { + expr_ref_pair_vector cofactors(m); + seq_rw().brz_derivative_cofactors(r, cofactors); + for (auto const& [c, t] : cofactors) { + if (!re().is_empty(t)) + targets.push_back(t); + } + } + + /* + Return a list of all (cond, leaf) pairs in a given derivative + expression r, where elem is the character symbol the derivative was + taken with respect to. + + The transition regexes produced by the symbolic derivative engine are + ITE-trees over character predicates ci on elem (equalities such as + elem = 'A', and ranges such as 'a' <= elem <= 'z'). These predicates + are typically mutually exclusive, so the number of feasible truth + assignments to {c1,..,ck} ("minterms") is small. + + The enumeration is delegated to seq::derive (via seq_rw().get_cofactors) + so it reuses the very same path/interval context that the derivative + engine uses while hoisting ITEs: each feasible path through the ITE-tree + yields one (path_condition, leaf) cofactor, infeasible character-range + combinations are pruned, and the leaf is simplified with the path-aware + smart constructors. + + This is used by: + propagate_is_empty + propagate_is_non_empty + */ /* is_empty(r, u) => ~is_nullable(r) @@ -750,11 +859,11 @@ namespace smt { d = mk_derivative_wrapper(hd, r); literal_vector lits; expr_ref_pair_vector cofactors(m); - get_cofactors(d, cofactors); - for (auto const& p : cofactors) { - if (is_member(p.second, u)) + seq_rw().get_cofactors(hd, d, cofactors); + for (auto const& [c, r] : cofactors) { + if (is_member(r, u)) continue; - expr_ref cond(p.first, m); + expr_ref cond(c, m); seq_rw().elim_condition(hd, cond); rewrite(cond); if (m.is_false(cond)) @@ -765,7 +874,7 @@ namespace smt { expr_ref ncond(mk_not(m, cond), m); lits.push_back(th.mk_literal(mk_forall(m, hd, ncond))); } - expr_ref is_empty1 = sk().mk_is_empty(p.second, re().mk_union(u, p.second), n); + expr_ref is_empty1 = sk().mk_is_empty(r, re().mk_union(u, r), n); lits.push_back(th.mk_literal(is_empty1)); th.add_axiom(lits); } @@ -878,50 +987,4 @@ namespace smt { return std::string("id") + std::to_string(e->get_id()); } - /** - Return a list of all (cond, leaf) pairs in a given - expression r. - - Note: this implementation is inefficient: it simply collects all expressions under an if and - iterates over all combinations. -*/ - void seq_regex::get_cofactors(expr *r, expr_ref_pair_vector &result) { - obj_hashtable ifs; - expr *cond = nullptr, *r1 = nullptr, *r2 = nullptr; - for (expr *e : subterms::ground(expr_ref(r, m))) - if (m.is_ite(e, cond, r1, r2)) - ifs.insert(cond); - - expr_ref_vector rs(m); - vector conds; - conds.push_back(expr_ref_vector(m)); - rs.push_back(r); - for (expr *c : ifs) { - unsigned sz = conds.size(); - expr_safe_replace rep1(m); - expr_safe_replace rep2(m); - rep1.insert(c, m.mk_true()); - rep2.insert(c, m.mk_false()); - expr_ref r2(m); - for (unsigned i = 0; i < sz; ++i) { - expr_ref_vector cs = conds[i]; - cs.push_back(m.mk_not(c)); - conds.push_back(cs); - conds[i].push_back(c); - expr_ref r1(rs.get(i), m); - rep1(r1, r2); - rs[i] = r2; - rep2(r1, r2); - rs.push_back(r2); - } - } - for (unsigned i = 0; i < conds.size(); ++i) { - expr_ref conj = mk_and(conds[i]); - expr_ref r(rs.get(i), m); - ctx.get_rewriter()(r); - if (!m.is_false(conj) && !re().is_empty(r)) - result.push_back(conj, r); - } - } - } diff --git a/src/smt/seq_regex.h b/src/smt/seq_regex.h index 74c4d7e74f..dd1c474b31 100644 --- a/src/smt/seq_regex.h +++ b/src/smt/seq_regex.h @@ -165,6 +165,12 @@ namespace smt { expr_ref mk_deriv_accept(expr* s, unsigned i, expr* r); void get_derivative_targets(expr* r, expr_ref_vector& targets); + // Decide emptiness of a ground regex by antimirov-mode NFA + // reachability: explore derivative target states, short-circuiting to + // "non-empty" on the first reachable nullable or classical state. + // Returns l_true (empty), l_false (non-empty), l_undef (gave up). + lbool re_is_empty(expr* r); + /* Pretty print the regex of the state id to the out stream, seq_regex_ptr must be a pointer to seq_regex and the @@ -183,9 +189,6 @@ namespace smt { bool block_if_empty(expr* r, literal lit); - void get_cofactors(expr *r, expr_ref_pair_vector &result); - - public: seq_regex(theory_seq& th); diff --git a/src/smt/smt_context.cpp b/src/smt/smt_context.cpp index 0add0fe6cd..4101aaadb4 100644 --- a/src/smt/smt_context.cpp +++ b/src/smt/smt_context.cpp @@ -119,6 +119,10 @@ namespace smt { if (!m_setup.already_configured()) { m_fparams.updt_params(p); } + else { + // selected parameters are safe to update after initialization + m_fparams.m_max_conflicts = p.get_uint("max_conflicts", m_fparams.m_max_conflicts); + } for (auto th : m_theory_set) if (th) th->updt_params(); @@ -3673,6 +3677,13 @@ namespace smt { } } + void context::setup_for_parallel() { + // Native SMT parallel configures the parent context before cloning workers. + // context::copy then configures/internalizes each worker copy while + // preprocessing is still enabled. + setup_context(m_fparams.m_auto_config); + } + config_mode context::get_config_mode(bool use_static_features) const { if (!m_fparams.m_auto_config) return CFG_BASIC; @@ -4693,7 +4704,6 @@ namespace smt { theory_id th_id = l->get_id(); for (enode * parent : enode::parents(n)) { - 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)) diff --git a/src/smt/smt_context.h b/src/smt/smt_context.h index 1bd967bdbe..2d2c0c815e 100644 --- a/src/smt/smt_context.h +++ b/src/smt/smt_context.h @@ -64,6 +64,7 @@ namespace smt { class model_generator; class context; + class kernel; struct oom_exception : public z3_error { oom_exception() : z3_error(ERR_MEMOUT) {} @@ -85,6 +86,7 @@ namespace smt { friend class model_generator; friend class lookahead; friend class parallel; + friend class kernel; public: statistics m_stats; @@ -294,6 +296,10 @@ namespace smt { return m_fparams; } + smt_params const& get_fparams() const { + return m_fparams; + } + params_ref const & get_params() { return m_params; } @@ -454,6 +460,8 @@ namespace smt { svector const & get_activity_vector() const { return m_activity; } double get_activity(bool_var v) const { return m_activity[v]; } + unsigned get_num_assignments() const { return m_stats.m_num_assignments; } + unsigned get_birthdate(bool_var v) const { return m_birthdate[v]; } void set_activity(bool_var v, double act) { m_activity[v] = act; } @@ -540,6 +548,8 @@ namespace smt { return m_scope_lvl == m_search_lvl; } + void pop_to_search_level() { pop_to_search_lvl(); } + bool tracking_assumptions() const { return !m_assumptions.empty() && m_search_lvl > m_base_lvl; } @@ -872,7 +882,7 @@ namespace smt { void undo_mk_enode(); - void apply_sort_cnstr(app * term, enode * e); + void apply_sort_cnstr(expr * term, enode * e); bool simplify_aux_clause_literals(unsigned & num_lits, literal * lits, literal_buffer & simp_lits); @@ -1699,6 +1709,8 @@ namespace smt { lbool setup_and_check(bool reset_cancel = true); + void setup_for_parallel(); + void reduce_assertions(); bool resource_limits_exceeded(); @@ -1917,5 +1929,3 @@ namespace smt { std::ostream& operator<<(std::ostream& out, enode_pp const& p); }; - - diff --git a/src/smt/smt_internalizer.cpp b/src/smt/smt_internalizer.cpp index 063b7297b7..3bb498882a 100644 --- a/src/smt/smt_internalizer.cpp +++ b/src/smt/smt_internalizer.cpp @@ -597,9 +597,10 @@ namespace smt { SASSERT(is_lambda(q)); if (e_internalized(q)) return; - mk_enode(q, true, /* do suppress args */ + auto e = mk_enode(q, true, /* do suppress args */ false, /* it is a term, so it should not be merged with true/false */ true); + apply_sort_cnstr(q, e); } bool context::has_lambda() { @@ -1084,8 +1085,8 @@ namespace smt { /** \brief Apply sort constraints on e. */ - void context::apply_sort_cnstr(app * term, enode * e) { - sort * s = term->get_decl()->get_range(); + void context::apply_sort_cnstr(expr * term, enode * e) { + sort * s = term->get_sort(); theory * th = m_theories.get_plugin(s->get_family_id()); if (th) { th->apply_sort_cnstr(e, s); diff --git a/src/smt/smt_kernel.cpp b/src/smt/smt_kernel.cpp index d61602f15d..904e2ce649 100644 --- a/src/smt/smt_kernel.cpp +++ b/src/smt/smt_kernel.cpp @@ -284,10 +284,22 @@ namespace smt { smt_params_helper::collect_param_descrs(d); } + void kernel::pop_to_base_level() { + m_imp->m_kernel.pop_to_base_lvl(); + } + + void kernel::set_preprocess(bool f) { + m_imp->m_kernel.get_fparams().m_preprocess = f; + } + context & kernel::get_context() { return m_imp->m_kernel; } + context const& kernel::get_context() const { + return m_imp->m_kernel; + } + void kernel::get_levels(ptr_vector const& vars, unsigned_vector& depth) { m_imp->m_kernel.get_levels(vars, depth); } diff --git a/src/smt/smt_kernel.h b/src/smt/smt_kernel.h index 98b6772132..0a6faaa091 100644 --- a/src/smt/smt_kernel.h +++ b/src/smt/smt_kernel.h @@ -300,6 +300,10 @@ namespace smt { */ static void collect_param_descrs(param_descrs & d); + void pop_to_base_level(); + + void set_preprocess(bool f); + void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause); /** @@ -340,6 +344,6 @@ namespace smt { \warning This method should not be used in new code. */ context & get_context(); + context const& get_context() const; }; }; - diff --git a/src/smt/smt_model_checker.cpp b/src/smt/smt_model_checker.cpp index c23cfe01e2..c6c914fe03 100644 --- a/src/smt/smt_model_checker.cpp +++ b/src/smt/smt_model_checker.cpp @@ -219,7 +219,7 @@ namespace smt { if (use_inv) { unsigned sk_term_gen = 0; - expr * sk_term = m_model_finder.get_inv(q, i, sk_value, sk_term_gen); + expr * sk_term = m_model_finder.get_inv(q, i, sk_value, *cex, sk_term_gen); if (sk_term != nullptr) { TRACE(model_checker, tout << "Found inverse " << mk_pp(sk_term, m) << "\n";); SASSERT(!m.is_model_value(sk_term)); @@ -233,15 +233,10 @@ namespace smt { } else { expr * sk_term = get_term_from_ctx(sk_value); - func_decl * f = nullptr; if (sk_term != nullptr) { TRACE(model_checker, tout << "sk term " << mk_pp(sk_term, m) << "\n"); sk_value = sk_term; } - // last ditch: am I an array? - else if (false && autil.is_as_array(sk_value, f) && cex->get_func_interp(f) && cex->get_func_interp(f)->get_array_interp(f)) { - sk_value = cex->get_func_interp(f)->get_array_interp(f); - } } if (contains_model_value(sk_value)) { diff --git a/src/smt/smt_model_finder.cpp b/src/smt/smt_model_finder.cpp index 27516b3dcd..4ba66a51be 100644 --- a/src/smt/smt_model_finder.cpp +++ b/src/smt/smt_model_finder.cpp @@ -18,6 +18,7 @@ Revision History: --*/ #include "util/backtrackable_set.h" #include "ast/ast_util.h" +#include "ast/has_free_vars.h" #include "ast/macros/macro_util.h" #include "ast/arith_decl_plugin.h" #include "ast/bv_decl_plugin.h" @@ -31,6 +32,7 @@ Revision History: #include "ast/ast_ll_pp.h" #include "ast/well_sorted.h" #include "ast/ast_smt2_pp.h" +#include "ast/rewriter/term_enumeration.h" #include "model/model_pp.h" #include "model/model_macro_solver.h" #include "smt/smt_model_finder.h" @@ -107,9 +109,15 @@ namespace smt { } } - expr* get_inv(expr* v) const { + expr* get_inv(expr* v, model& mdl) const { expr* t = nullptr; m_inv.find(v, t); + if (!t) { + for (auto [k, term] : m_inv) { + if (mdl.are_equal(k, v)) + return term; + } + } return t; } @@ -120,14 +128,11 @@ namespace smt { } void mk_inverse(evaluator& ev) { - for (auto const& kv : m_elems) { - expr* t = kv.m_key; + for (auto const &[t, gen] : m_elems) { SASSERT(!contains_model_value(t)); - unsigned gen = kv.m_value; expr* t_val = ev.eval(t, true); if (!t_val) break; TRACE(model_finder, tout << mk_pp(t, m) << " " << mk_pp(t_val, m) << "\n";); - expr* old_t = nullptr; if (m_inv.find(t_val, old_t)) { unsigned old_t_gen = 0; @@ -187,14 +192,14 @@ namespace smt { \brief Base class used to solve model construction constraints. */ class node { - unsigned m_id; - node* m_find{ nullptr }; - unsigned m_eqc_size{ 1 }; + unsigned m_id = 0; + node* m_find = nullptr; + unsigned m_eqc_size = 1; - sort* m_sort; // sort of the elements in the instantiation set. + sort* m_sort = nullptr; // sort of the elements in the instantiation set. - bool m_mono_proj{ false }; // relevant for integers & reals & bit-vectors - bool m_signed_proj{ false }; // relevant for bit-vectors. + bool m_mono_proj = false; // relevant for integers & reals & bit-vectors + bool m_signed_proj = false; // relevant for bit-vectors. ptr_vector m_avoid_set; ptr_vector m_exceptions; @@ -291,7 +296,7 @@ namespace smt { } void insert(expr* n, unsigned generation) { - if (is_ground(n)) + if (is_ground(n) || (has_quantifiers(n) && !has_free_vars(n))) // this is a closed term get_root()->m_set->insert(n, generation); } @@ -599,7 +604,10 @@ namespace smt { } else { r = tmp; - TRACE(model_finder, tout << "eval\n" << mk_pp(n, m) << "\n----->\n" << mk_pp(r, m) << "\n";); + TRACE(model_finder, tout << "eval-failed\n" << mk_pp(n, m) << "\n----->\n" << mk_pp(r, m) << "\n";); + if (is_lambda(tmp)) { + r = m.mk_fresh_const("lambda", tmp->get_sort()); + } } m_eval_cache[model_completion].insert(n, r); m_eval_cache_range.push_back(r); @@ -1235,8 +1243,8 @@ namespace smt { void populate_inst_sets(quantifier* q, func_decl* mhead, ptr_vector& uvar_inst_sets, context* ctx) override { if (m_f != mhead) return; - uvar_inst_sets.reserve(m_var_j + 1, 0); - if (uvar_inst_sets[m_var_j] == 0) + uvar_inst_sets.reserve(m_var_j + 1, nullptr); + if (uvar_inst_sets[m_var_j] == nullptr) uvar_inst_sets[m_var_j] = alloc(instantiation_set, ctx->get_manager()); instantiation_set* s = uvar_inst_sets[m_var_j]; SASSERT(s != nullptr); @@ -1369,6 +1377,81 @@ namespace smt { }; + class ho_var : public qinfo { + unsigned m_var_i; + public: + ho_var(ast_manager& m, unsigned i) : qinfo(m), m_var_i(i) { + } + + char const *get_kind() const override { + return "ho_var"; + } + + bool is_equal(qinfo const *qi) const override { + if (qi->get_kind() != get_kind()) + return false; + ho_var const *other = static_cast(qi); + return m_var_i == other->m_var_i; + } + + void display(std::ostream &out) const override { + out << "(" << "ho-var: " << m_var_i << ")"; + } + + void process_auf(quantifier *q, auf_solver &s, context *ctx) override { + /* node * S_i = */ s.get_uvar(q, m_var_i); + } + + void populate_inst_sets(quantifier *q, auf_solver &s, context *ctx) override { + node *S = s.get_uvar(q, m_var_i); + sort *srt = S->get_sort(); + + IF_VERBOSE(3, verbose_stream() << "ho_var::populate_inst_sets: " << q->get_id() << " " << mk_pp(srt, m) << "\n";); + term_enumeration tn(m); + // Add ground terms of type S. + // Add productions for functions in E-graph + // add other possible relevant functions such as equality over srt, Boolean operators + + ast_mark visited; + tn.add_production(m.mk_true()); + tn.add_production(m.mk_false()); + for (enode *n : ctx->enodes()) { + if (!ctx->is_relevant(n)) + continue; + auto e = n->get_expr(); + if (srt == n->get_sort()) { + TRACE(model_finder, tout << "inserting " << mk_pp(e, m) << " into inst set\n"); + S->insert(e, n->get_generation()); + } + else if (is_app(e) && to_app(e)->get_decl()->is_skolem()) + ; + else if (is_uninterp_const(e)) { + TRACE(model_finder, tout << "add production " << mk_pp(e, m) << "\n"); + tn.add_production(e); + } + else if (is_uninterp(e)) { + auto f = to_app(e)->get_decl(); + if (visited.is_marked(f)) + continue; + visited.mark(f, true); + TRACE(model_finder, tout << "add function " << mk_pp(f, m) << "\n"); + tn.add_production(f); + } + } + + unsigned max_count = 20; + for (auto t : tn.enum_terms(srt)) { + if (max_count == 0) + break; + --max_count; + unsigned generation = 0; // todo - inherited from sub-term of t? + TRACE(model_finder, tout << "ho_var: adding term " << mk_ismt2_pp(t, m) + << " to instantiation set of S" << std::endl;); + S->insert(t, generation); + } + } + }; + /** \brief auf_arr is a term (pattern) of the form: @@ -2105,7 +2188,10 @@ namespace smt { process_app(to_app(curr)); } else if (is_var(curr)) { - m_info->m_is_auf = false; // unexpected occurrence of variable. + if (m_array_util.is_array(curr)) { + insert_qinfo(alloc(ho_var, m, to_var(curr)->get_idx())); + } + m_info->m_is_auf = false; } else { SASSERT(is_lambda(curr)); @@ -2163,7 +2249,6 @@ namespace smt { } SASSERT(is_quantifier(atom)); - UNREACHABLE(); } void process_literal(expr* atom, polarity pol) { @@ -2203,9 +2288,15 @@ namespace smt { if (is_app(curr)) { if (to_app(curr)->get_family_id() == m.get_basic_family_id() && m.is_bool(curr)) { switch (static_cast(to_app(curr)->get_decl_kind())) { - case OP_IMPLIES: + case OP_IMPLIES: + process_literal(to_app(curr)->get_arg(0), neg(pol)); + process_literal(to_app(curr)->get_arg(1), pol); + break; case OP_XOR: - UNREACHABLE(); // simplifier eliminated ANDs, IMPLIEs, and XORs + for (expr *arg : *to_app(curr)) { + visit_formula(arg, pol); + visit_formula(arg, neg(pol)); + } break; case OP_OR: case OP_AND: @@ -2515,11 +2606,12 @@ namespace smt { Store in generation the generation of the result */ - expr* model_finder::get_inv(quantifier* q, unsigned i, expr* val, unsigned& generation) { + expr* model_finder::get_inv(quantifier* q, unsigned i, expr* val, model& mdl,unsigned& generation) { instantiation_set const* s = get_uvar_inst_set(q, i); if (s == nullptr) return nullptr; - expr* t = s->get_inv(val); + + expr* t = s->get_inv(val, mdl); if (m_auf_solver->is_default_representative(t)) return val; if (t != nullptr) { @@ -2555,16 +2647,27 @@ namespace smt { obj_map const& inv = s->get_inv_map(); if (inv.empty()) continue; // nothing to do - ptr_buffer eqs; - for (auto const& [val, _] : inv) { - if (val->get_sort() == sk->get_sort()) - eqs.push_back(m.mk_eq(sk, val)); + expr_ref_vector eqs(m), defs(m); + + for (auto const& [val, term] : inv) { + if (val->get_sort() == sk->get_sort()) { + if (is_lambda(term)) { + eqs.push_back(m.mk_eq(sk, val)); + defs.push_back(m.mk_eq(val, term)); + } + else + eqs.push_back(m.mk_eq(sk, val)); + } } if (!eqs.empty()) { expr_ref new_cnstr(m); new_cnstr = m.mk_or(eqs); TRACE(model_finder, tout << "assert_restriction:\n" << mk_pp(new_cnstr, m) << "\n";); aux_ctx->assert_expr(new_cnstr); + for (auto def : defs) { + TRACE(model_finder, tout << "assert_def:\n" << mk_pp(def, m) << "\n";); + aux_ctx->assert_expr(def); + } asserted_something = true; } } diff --git a/src/smt/smt_model_finder.h b/src/smt/smt_model_finder.h index 1c468fc648..3b34e0192f 100644 --- a/src/smt/smt_model_finder.h +++ b/src/smt/smt_model_finder.h @@ -113,7 +113,7 @@ namespace smt { void fix_model(proto_model * m); quantifier * get_flat_quantifier(quantifier * q); - expr * get_inv(quantifier * q, unsigned i, expr * val, unsigned & generation); + expr * get_inv(quantifier * q, unsigned i, expr * val, model& m, unsigned & generation); bool restrict_sks_to_inst_set(context * aux_ctx, quantifier * q, expr_ref_vector const & sks); void restart_eh(); diff --git a/src/smt/smt_parallel.cpp b/src/smt/smt_parallel.cpp index 64848d8902..5096d23144 100644 --- a/src/smt/smt_parallel.cpp +++ b/src/smt/smt_parallel.cpp @@ -25,7 +25,7 @@ Author: #include "smt/smt_parallel.h" #include "smt/smt_lookahead.h" #include "solver/solver_preprocess.h" -#include "params/smt_parallel_params.hpp" +#include "solver/parallel_params.hpp" #include #include @@ -550,7 +550,7 @@ namespace smt { 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)) + if (m_search_tree.is_lease_canceled(target.leased_node)) continue; // Reconstruct the full path from root to this target node @@ -626,7 +626,7 @@ namespace smt { 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; + ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged } void parallel::core_minimizer_worker::cancel() { @@ -763,26 +763,42 @@ namespace smt { if (m_config.m_global_backbones) { bb_candidates local_candidates = find_backbone_candidates(); b.collect_backbone_candidates(m_l2g, local_candidates); - if (!m.inc()) + bool lease_canceled = false; + if (!b.checkpoint_worker(id, lease, lease_canceled)) return; + if (lease_canceled) { + LOG_WORKER(1, " abandoning canceled lease\n"); + continue; + } } lbool r = check_cube(cube); - if (b.lease_canceled(lease)) { + bool lease_canceled = false; + if (!b.checkpoint_worker(id, lease, lease_canceled)) + return; + if (lease_canceled) { 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(); LOG_WORKER(1, " found undef cube\n"); + // Escalating the per-thread conflict budget and re-splitting the + // cube only helps when the cube was abandoned because the per-cube + // conflict limit was reached. For any other source of incompleteness + // (an incomplete theory, quantifiers, lambdas, resource limits, ...) + // the verdict cannot change, so re-checking the same cube would spin + // forever and the run hangs to a wall-clock timeout. Record a sound + // 'unknown' verdict and stop working this branch instead. + std::string reason = ctx->last_failure_as_string(); + if (reason != "max-conflicts-reached") { + LOG_WORKER(1, " undef cube not conflict-limited (" << reason << "); reporting unknown\n"); + b.set_unknown(reason); + return; + } + update_max_thread_conflicts(); if (m_config.m_max_cube_depth <= cube.size()) goto check_cube_start; @@ -790,7 +806,6 @@ namespace smt { if (!atom) goto check_cube_start; b.try_split(m_l2g, id, lease, atom, m_config.m_threads_max_conflicts); - lease = {}; simplify(); break; } @@ -825,7 +840,6 @@ namespace smt { 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))); @@ -854,10 +868,10 @@ namespace smt { 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(); + parallel_params pp(p.ctx.m_params); + m_config.m_inprocessing = false; + m_config.m_global_backbones = pp.num_bb_threads() > 0; + m_config.m_local_backbones = false; m_config.m_core_minimize = pp.core_minimize(); m_config.m_ablate_backtracking = pp.ablate_backtracking(); @@ -887,9 +901,9 @@ namespace smt { ctx->pop_to_base_lvl(); 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_use_failed_literal_test = pp.num_global_bb_fl_threads() > 0; + m_use_failed_literal_test = false; } parallel::bb_candidates parallel::worker::find_backbone_candidates(unsigned k) { @@ -1105,14 +1119,48 @@ namespace smt { return r; } - void parallel::batch_manager::release_lease_unlocked(unsigned worker_id, node* n) { - if (worker_id >= m_worker_leases.size()) + void parallel::batch_manager::set_canceled_unlocked() { + if (m_state != state::is_running) return; - auto &lease = m_worker_leases[worker_id]; - if (!lease.leased_node || lease.leased_node != n) + cancel_background_threads(); + } + + void parallel::batch_manager::set_canceled() { + std::scoped_lock lock(mux); + set_canceled_unlocked(); + } + + void parallel::batch_manager::release_worker_lease_unlocked(unsigned worker_id, node_lease& lease) { + if (worker_id >= m_worker_leases.size()) { + lease = {}; return; - m_search_tree.dec_active_workers(lease.leased_node); + } + auto& stored_lease = m_worker_leases[worker_id]; + if (!stored_lease.leased_node || stored_lease.leased_node != lease.leased_node) { + lease = {}; + return; + } + bool cancel_signaled = stored_lease.cancel_signaled; + m_search_tree.dec_active_workers(stored_lease.leased_node); + stored_lease = {}; lease = {}; + if (cancel_signaled) + p.m_workers[worker_id]->limit().dec_cancel(); + } + + bool parallel::batch_manager::attempt_release_canceled_lease_unlocked(unsigned worker_id, node_lease& lease) { + if (m_state != state::is_running || !lease.leased_node || worker_id >= m_worker_leases.size()) + return false; + + auto& stored_lease = m_worker_leases[worker_id]; + if (stored_lease.leased_node != lease.leased_node) + return false; + + if (!m_search_tree.is_lease_canceled(stored_lease.leased_node)) + return false; + + release_worker_lease_unlocked(worker_id, lease); + return true; } void parallel::batch_manager::cancel_closed_leases_unlocked(unsigned source_worker_id) { @@ -1124,7 +1172,7 @@ namespace smt { // 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)) { + if (lease.leased_node && !lease.cancel_signaled && m_search_tree.is_lease_canceled(lease.leased_node)) { p.m_workers[worker_id]->cancel_lease(); m_worker_leases[worker_id].cancel_signaled = true; } @@ -1132,7 +1180,7 @@ namespace smt { } void parallel::batch_manager::backtrack(ast_translation &l2g, unsigned worker_id, expr_ref_vector const &core, - node_lease const &lease) { + node_lease& lease) { std::scoped_lock lock(mux); vector g_core; for (auto c : core) @@ -1277,7 +1325,7 @@ namespace smt { 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)) + if (!m_search_tree.is_lease_canceled(target.leased_node)) m_search_tree.backtrack(target.leased_node, g_core); } } @@ -1331,7 +1379,7 @@ namespace smt { for (node* t : matches) { if (!t || t == source) continue; - if (m_search_tree.is_lease_canceled(t, t->get_cancel_epoch())) + if (m_search_tree.is_lease_canceled(t)) continue; // When source is provided, keep only external matches. Nodes in the @@ -1358,12 +1406,12 @@ namespace smt { if (!is_highest_ancestor) continue; - targets.push_back({ t, t->get_cancel_epoch() }); + targets.push_back({t}); } } void parallel::batch_manager::backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, - node_lease const* lease, vector const* targets) { + node_lease* lease, vector const* targets) { if (m_state != state::is_running) return; @@ -1374,17 +1422,25 @@ namespace smt { 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 (lease) { + if (!m_search_tree.is_lease_canceled(lease->leased_node)) { + // 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"); + node* leased_node = lease->leased_node; + release_worker_lease_unlocked(worker_id, *lease); + m_search_tree.backtrack(leased_node, g_core); + } + else { + // the lease was canceled by another worker. don't backtrack on this node with whatever new core we just found with this thread + // however, we do proceed to external targets, since the new code may have exposed new external targets we can close/backtrack + attempt_release_canceled_lease_unlocked(worker_id, *lease); + } } if (targets) { for (auto const& target : *targets) { - if (m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch)) + if (m_search_tree.is_lease_canceled(target.leased_node)) continue; did_backtrack = true; @@ -1410,37 +1466,59 @@ namespace smt { } void parallel::batch_manager::try_split(ast_translation &l2g, unsigned worker_id, - node_lease const &lease, expr *atom, unsigned effort) { + node_lease& 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)) + if (m_search_tree.is_lease_canceled(lease.leased_node)) { + attempt_release_canceled_lease_unlocked(worker_id, lease); 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); + node* leased_node = lease.leased_node; + VERIFY(!leased_node->path_contains_atom(lit)); + VERIFY(!leased_node->path_contains_atom(nlit)); + bool did_split = m_search_tree.try_split(leased_node, lit, nlit, effort); - release_lease_unlocked(worker_id, lease.leased_node); + release_worker_lease_unlocked(worker_id, lease); 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); + m_stats.m_max_cube_depth = std::max(m_stats.m_max_cube_depth, 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) { + bool parallel::batch_manager::checkpoint_worker(unsigned worker_id, node_lease& lease, bool& lease_canceled) { std::scoped_lock lock(mux); - release_lease_unlocked(worker_id, lease.leased_node); + lease_canceled = false; + SASSERT(worker_id < p.m_workers.size()); + + if (attempt_release_canceled_lease_unlocked(worker_id, lease)) { + lease_canceled = true; + return true; + } + + if (p.m_workers[worker_id]->limit().inc()) + return true; + + if (attempt_release_canceled_lease_unlocked(worker_id, lease)) { + lease_canceled = true; + return true; + } + + set_canceled_unlocked(); + return false; } 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); + return m_state == state::is_running && m_search_tree.is_lease_canceled(lease.leased_node); } void parallel::batch_manager::collect_clause(ast_translation &l2g, unsigned source_worker_id, expr *clause) { @@ -1662,7 +1740,7 @@ namespace smt { void parallel::batch_manager::set_sat(ast_translation &l2g, model &m) { std::scoped_lock lock(mux); IF_VERBOSE(1, verbose_stream() << "Batch manager setting SAT.\n"); - if (m_state != state::is_running) + if (m_state != state::is_running && m_state != state::is_unknown) return; m_state = state::is_sat; p.ctx.set_model(m.translate(l2g)); @@ -1672,7 +1750,7 @@ namespace smt { void parallel::batch_manager::set_unsat(ast_translation &l2g, expr_ref_vector const &unsat_core) { std::scoped_lock lock(mux); IF_VERBOSE(1, verbose_stream() << "Batch manager setting UNSAT.\n"); - if (m_state != state::is_running) + if (m_state != state::is_running && m_state != state::is_unknown) return; m_state = state::is_unsat; @@ -1683,6 +1761,16 @@ namespace smt { cancel_background_threads(); } + void parallel::batch_manager::set_unknown(std::string const &reason) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "Batch manager setting UNKNOWN: " << reason << ".\n"); + if (m_state != state::is_running) + return; // a definitive sat/unsat verdict or exception already won. + m_state = state::is_unknown; + m_reason_unknown = reason; + cancel_background_threads(); + } + void parallel::batch_manager::set_exception(unsigned error_code) { std::scoped_lock lock(mux); IF_VERBOSE(1, verbose_stream() << "Batch manager setting exception code: " << error_code << ".\n"); @@ -1714,6 +1802,8 @@ namespace smt { return l_false; case state::is_sat: return l_true; + case state::is_unknown: + return l_undef; case state::is_exception_msg: throw default_exception(m_exception_msg.c_str()); case state::is_exception_code: @@ -1745,7 +1835,6 @@ namespace smt { 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; @@ -1779,8 +1868,9 @@ namespace smt { m_worker_leases.reset(); m_worker_leases.resize(p.m_workers.size()); - smt_parallel_params pp(p.ctx.m_params); + parallel_params pp(p.ctx.m_params); m_ablate_backtracking = pp.ablate_backtracking(); + m_canceled = false; } void parallel::batch_manager::collect_statistics(::statistics &st) const { @@ -1794,20 +1884,26 @@ namespace smt { } lbool parallel::operator()(expr_ref_vector const &asms) { - 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); + parallel_params pp(ctx.m_params); + unsigned num_global_bb_threads = pp.num_bb_threads(); + if (num_global_bb_threads > 2) + throw default_exception("parallel.num_bb_threads must be 0, 1, or 2"); + unsigned total_threads = std::min((unsigned)std::thread::hardware_concurrency(), ctx.get_fparams().m_threads); + unsigned num_workers = total_threads; + unsigned num_sls_threads = 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 (num_workers > 2 + num_core_min_threads) + num_workers -= num_core_min_threads; + else + num_core_min_threads = 0; + if (num_workers > 2 + num_global_bb_threads) + num_workers -= num_global_bb_threads; + else + num_global_bb_threads = 0; + if (num_workers > 2 + num_sls_threads) + num_workers -= num_sls_threads; + else + num_sls_threads = 0; IF_VERBOSE(1, verbose_stream() << "Parallel SMT with " << total_threads << " threads\n";); ast_manager &m = ctx.m; @@ -1841,7 +1937,7 @@ namespace smt { m_sls_worker = alloc(sls_worker, *this); sl.push_child(&(m_sls_worker->limit())); } - if (pp.core_minimize()) { + if (num_core_min_threads == 1) { m_core_minimizer_worker = alloc(core_minimizer_worker, *this, asms); sl.push_child(&(m_core_minimizer_worker->limit())); } @@ -1856,18 +1952,52 @@ namespace smt { << m_global_backbones_workers.size() << " global backbone threads.\n";); m_batch_manager.initialize(num_global_bb_threads); + + auto safe_run = [&](auto&& run_fn, reslimit& lim) { + try { + run_fn(); + if (lim.is_canceled()) + m_batch_manager.set_canceled(); + } catch (z3_error &err) { + IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << err.what() << "\n"); + if (!lim.is_canceled()) + m_batch_manager.set_exception(err.error_code()); + else + m_batch_manager.set_canceled(); + } catch (z3_exception &ex) { + IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << ex.what() << "\n"); + if (!lim.is_canceled() && !is_cancellation_exception(ex.what())) + m_batch_manager.set_exception(ex.what()); + else + m_batch_manager.set_canceled(); + } catch (...) { + IF_VERBOSE(0, verbose_stream() << "Unknown exception in parallel solver\n"); + if (!lim.is_canceled()) + m_batch_manager.set_exception("unknown exception"); + else + m_batch_manager.set_canceled(); + } + }; // Launch threads vector threads(total_threads); unsigned thread_idx = 0; for (auto* w : m_workers) - threads[thread_idx++] = std::thread([&, w]() { w->run(); }); + threads[thread_idx++] = std::thread([w, &safe_run]() { + safe_run([w]() { w->run(); }, w->limit()); + }); if (m_sls_worker) - threads[thread_idx++] = std::thread([&]() { m_sls_worker->run(); }); + threads[thread_idx++] = std::thread([this, &safe_run]() { + safe_run([this]() { m_sls_worker->run(); }, m_sls_worker->limit()); + }); if (m_core_minimizer_worker) - threads[thread_idx++] = std::thread([&]() { m_core_minimizer_worker->run(); }); + threads[thread_idx++] = std::thread([this, &safe_run]() { + safe_run([this]() { m_core_minimizer_worker->run(); }, m_core_minimizer_worker->limit()); + }); for (auto* w : m_global_backbones_workers) - threads[thread_idx++] = std::thread([&, w]() { w->run(); }); + threads[thread_idx++] = std::thread([w, &safe_run]() { + safe_run([w]() { w->run(); }, w->limit()); + }); // Wait for all threads to finish @@ -1884,7 +2014,10 @@ namespace smt { for (auto* bb_w : m_global_backbones_workers) bb_w->collect_statistics(ctx.m_aux_stats); - return m_batch_manager.get_result(); + lbool result = m_batch_manager.get_result(); + if (result == l_undef && !m_batch_manager.get_reason_unknown().empty()) + ctx.set_reason_unknown(m_batch_manager.get_reason_unknown().c_str()); + return result; } } // namespace smt diff --git a/src/smt/smt_parallel.h b/src/smt/smt_parallel.h index 64fcc11869..4af58efbc9 100644 --- a/src/smt/smt_parallel.h +++ b/src/smt/smt_parallel.h @@ -32,6 +32,13 @@ namespace smt { struct cube_config { using literal = expr_ref; static bool literal_is_null(expr_ref const& l) { return l == nullptr; } + static bool same_atom(expr_ref const& a, expr_ref const& b) { + expr* atom_a = a.get(); + expr* atom_b = b.get(); + a.get_manager().is_not(atom_a, atom_a); + b.get_manager().is_not(atom_b, atom_b); + return atom_a == atom_b; + } static std::ostream& display_literal(std::ostream& out, expr_ref const& l) { return out << mk_bounded_pp(l, l.get_manager()); } }; @@ -74,6 +81,7 @@ namespace smt { is_running, is_sat, is_unsat, + is_unknown, is_exception_msg, is_exception_code }; @@ -102,6 +110,7 @@ namespace smt { unsigned m_exception_code = 0; std::string m_exception_msg; + std::string m_reason_unknown; vector shared_clause_trail; // store all shared clauses with worker IDs obj_hashtable shared_clause_set; // for duplicate filtering on per-thread clause expressions @@ -145,7 +154,11 @@ namespace smt { w->cancel(); } + std::atomic m_canceled = false; + void cancel_background_threads() { + if (m_canceled.exchange(true)) + return; // already canceled cancel_workers(); cancel_sls_worker(); if (!p.m_global_backbones_workers.empty()) { @@ -171,9 +184,11 @@ namespace smt { } void backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, - node_lease const* lease = nullptr, vector const* targets = nullptr); + node_lease* 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 set_canceled_unlocked(); + void release_worker_lease_unlocked(unsigned worker_id, node_lease& lease); + bool attempt_release_canceled_lease_unlocked(unsigned worker_id, node_lease& lease); void cancel_closed_leases_unlocked(unsigned source_worker_id); void collect_matching_targets_unlocked(node* source, expr* lit, vector const& core, vector& targets); @@ -187,6 +202,8 @@ namespace smt { void set_unsat(ast_translation& l2g, expr_ref_vector const& unsat_core); void set_sat(ast_translation& l2g, model& m); + void set_unknown(std::string const& reason); + void set_canceled(); void set_exception(std::string const& msg); void set_exception(unsigned error_code); void collect_statistics(::statistics& st) const; @@ -210,20 +227,21 @@ namespace smt { } 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 backtrack(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, node_lease& 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); + void try_split(ast_translation& l2g, unsigned worker_id, node_lease& lease, expr* atom, unsigned effort); + bool checkpoint_worker(unsigned worker_id, node_lease& lease, bool& lease_canceled); 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; + std::string const& get_reason_unknown() const { return m_reason_unknown; } bool is_global_backbone_or_negation(ast_translation& l2g, expr* bb_cand) { std::scoped_lock lock(mux); diff --git a/src/smt/smt_solver.cpp b/src/smt/smt_solver.cpp index d1e5e5a6bd..393fff202a 100644 --- a/src/smt/smt_solver.cpp +++ b/src/smt/smt_solver.cpp @@ -22,12 +22,15 @@ Notes: #include "ast/for_each_expr.h" #include "ast/ast_pp.h" #include "ast/func_decl_dependencies.h" +#include "smt/smt_context.h" #include "smt/smt_kernel.h" #include "params/smt_params.h" #include "params/smt_params_helper.hpp" #include "solver/solver_na2as.h" #include "solver/mus.h" +#include + namespace { class smt_solver : public solver_na2as { @@ -61,6 +64,7 @@ namespace { smt_params m_smt_params; smt::kernel m_context; cuber* m_cuber; + random_gen m_rand; symbol m_logic; bool m_minimizing_core; bool m_core_extend_patterns; @@ -84,16 +88,19 @@ namespace { updt_params(p); } - solver * translate(ast_manager & m, params_ref const & p) override { - ast_translation translator(get_manager(), m); + solver * translate(ast_manager & target, params_ref const & p) override { + ast_translation translator(get_manager(), target); + params_ref init; + init.copy(get_params()); + init.copy(p); - smt_solver * result = alloc(smt_solver, m, p, m_logic); + smt_solver* result = alloc(smt_solver, target, init, m_logic); smt::kernel::copy(m_context, result->m_context, true); - if (mc0()) + if (mc0()) result->set_model_converter(mc0()->translate(translator)); - for (auto & [k, v] : m_name2assertion) { + for (auto& [k, v] : m_name2assertion) { expr* val = translator(k); expr* key = translator(v); result->assert_expr(val, key); @@ -212,6 +219,97 @@ namespace { return m_context.get_trail(max_level); } + expr_ref_vector get_assigned_literals() override { + expr_ref_vector result(m); + auto const& ctx = m_context.get_context(); + for (auto lit : ctx.assigned_literals()) { + expr* atom = ctx.bool_var2expr(lit.var()); + if (!atom) + continue; + result.push_back(lit.sign() ? m.mk_not(atom) : atom); + } + return result; + } + + unsigned get_assign_level(expr* e) const override { + auto const& ctx = m_context.get_context(); + get_manager().is_not(e, e); + if (!ctx.b_internalized(e)) + return UINT_MAX; + return ctx.get_assign_level(ctx.get_bool_var(e)); + } + + bool is_relevant(expr* e) const override { + auto const& ctx = m_context.get_context(); + get_manager().is_not(e, e); + return ctx.b_internalized(e) && ctx.is_relevant(e); + } + + unsigned get_num_bool_vars() const override { + return m_context.get_context().get_num_bool_vars(); + } + + sat::bool_var get_bool_var(expr* e) const override { + auto const& ctx = m_context.get_context(); + get_manager().is_not(e, e); + return ctx.b_internalized(e) ? ctx.get_bool_var(e) : sat::null_bool_var; + } + + void pop_to_base_level() override { + m_context.pop_to_base_level(); + } + + void setup_for_parallel() override { + m_context.get_context().setup_for_parallel(); + } + + void set_preprocess(bool f) override { + m_context.set_preprocess(f); + } + + void set_max_conflicts(unsigned max_conflicts) override { + auto& ctx = m_context.get_context(); + ctx.get_fparams().m_max_conflicts = max_conflicts; + } + + unsigned get_max_conflicts() const override { + return m_context.get_context().get_fparams().m_max_conflicts; + } + + void get_backbone_candidates(vector& candidates, unsigned max_num) override { + ast_manager& m = get_manager(); + auto& ctx = m_context.get_context(); + unsigned curr_time = ctx.get_num_assignments(); + vector all; + + for (unsigned v = 0; v < ctx.get_num_bool_vars(); ++v) { + if (ctx.get_assignment(v) != l_undef && ctx.get_assign_level(v) == ctx.get_base_level()) + continue; + + expr* candidate = ctx.bool_var2expr(v); + if (!candidate) + continue; + + auto const& d = ctx.get_bdata(v); + if (d.m_phase_available && !d.m_phase) + candidate = m.mk_not(candidate); + + double age = static_cast(curr_time - ctx.get_birthdate(v)); + all.push_back(solver::scored_literal(m, candidate, age)); + } + + std::stable_sort( + all.begin(), + all.end(), + [](solver::scored_literal const& a, solver::scored_literal const& b) { + return a.score > b.score; + }); + + unsigned n = std::min(max_num, all.size()); + for (unsigned i = 0; i < n; ++i) + candidates.push_back(all[i]); + } + void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause) override { m_context.register_on_clause(ctx, on_clause); } @@ -368,6 +466,39 @@ namespace { return lits; } + expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { + ast_manager& m = get_manager(); + auto& ctx = m_context.get_context(); + obj_hashtable invalid_split_atoms_set; + for (expr* e : invalid_split_atoms) { + expr* atom = e; + m.is_not(e, atom); + invalid_split_atoms_set.insert(atom); + } + expr_ref result(m); + double score = 0.0; + unsigned n = 0; + + ctx.pop_to_search_level(); + for (unsigned v = 0; v < ctx.get_num_bool_vars(); ++v) { + if (ctx.get_assignment(v) != l_undef) + continue; + expr* e = ctx.bool_var2expr(v); + if (!e) + continue; + expr* atom = e; + m.is_not(e, atom); + if (invalid_split_atoms_set.contains(atom)) + continue; + double new_score = ctx.get_activity(v); + if (new_score > score || !result || (new_score == score && m_rand(++n) == 0)) { + score = new_score; + result = e; + } + } + return result; + } + struct collect_fds_proc { ast_manager & m; func_decl_set & m_fds; @@ -537,4 +668,3 @@ public: solver_factory * mk_smt_solver_factory() { return alloc(smt_solver_factory); } - diff --git a/src/smt/tactic/smt_tactic_core.cpp b/src/smt/tactic/smt_tactic_core.cpp index 6a624b59a9..ddef907a81 100644 --- a/src/smt/tactic/smt_tactic_core.cpp +++ b/src/smt/tactic/smt_tactic_core.cpp @@ -31,7 +31,6 @@ 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 @@ -431,8 +430,6 @@ 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); } @@ -440,8 +437,6 @@ tactic * mk_smt_tactic_core(ast_manager& m, params_ref const& p, symbol const& l parallel_params pp(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); } @@ -450,7 +445,7 @@ tactic * mk_smt_tactic_core_using(ast_manager& m, bool auto_config, params_ref c params_ref p = _p; p.set_bool("auto_config", auto_config); tactic *t = nullptr; - if (pp.enable() || pp.enable2()) + if (pp.enable()) t = mk_parallel_smt_tactic(m, p); else t = mk_seq_smt_tactic(m, p); diff --git a/src/smt/theory_array.cpp b/src/smt/theory_array.cpp index 2b9f5ba516..d35da46796 100644 --- a/src/smt/theory_array.cpp +++ b/src/smt/theory_array.cpp @@ -396,15 +396,15 @@ namespace smt { } final_check_status theory_array::assert_delayed_axioms() { - if (!m_params.m_array_delay_exp_axiom) - return FC_DONE; final_check_status r = FC_DONE; - unsigned num_vars = get_num_vars(); - for (unsigned v = 0; v < num_vars; ++v) { - var_data * d = m_var_data[v]; - if (d->m_prop_upward && instantiate_axiom2b_for(v)) - r = FC_CONTINUE; - } + if (m_params.m_array_delay_exp_axiom) { + unsigned num_vars = get_num_vars(); + for (unsigned v = 0; v < num_vars; ++v) { + var_data *d = m_var_data[v]; + if (d->m_prop_upward && instantiate_axiom2b_for(v)) + r = FC_CONTINUE; + } + } return r; } diff --git a/src/smt/theory_array.h b/src/smt/theory_array.h index d6ce0f4b9f..88f5678aea 100644 --- a/src/smt/theory_array.h +++ b/src/smt/theory_array.h @@ -29,6 +29,7 @@ namespace smt { 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, m_num_choice_axiom; + unsigned m_num_select_lambda_axiom; void reset() { memset(this, 0, sizeof(theory_array_stats)); } theory_array_stats() { reset(); } }; diff --git a/src/smt/theory_array_base.cpp b/src/smt/theory_array_base.cpp index 1bfa055841..19e89bd656 100644 --- a/src/smt/theory_array_base.cpp +++ b/src/smt/theory_array_base.cpp @@ -67,7 +67,6 @@ namespace smt { return mk_select(num_args, args); } - app * theory_array_base::mk_store(unsigned num_args, expr * const * args) { return m.mk_app(get_family_id(), OP_STORE, 0, nullptr, num_args, args); } @@ -279,7 +278,7 @@ namespace smt { SASSERT(n1->get_num_args() == n2->get_num_args()); unsigned n = n1->get_num_args(); // skipping first argument of the select. - for(unsigned i = 1; i < n; ++i) { + for (unsigned i = 1; i < n; ++i) { if (n1->get_arg(i)->get_root() != n2->get_arg(i)->get_root()) { return false; } @@ -295,9 +294,8 @@ namespace smt { enode * r1 = v1->get_root(); enode * r2 = v2->get_root(); - if (r1->get_class_size() > r2->get_class_size()) { - std::swap(r1, r2); - } + if (r1->get_class_size() > r2->get_class_size()) + std::swap(r1, r2); m_array_value.reset(); // populate m_array_value if the select(a, i) parent terms of r1 @@ -335,7 +333,7 @@ namespace smt { return false; // axiom was already instantiated if (already_diseq(n1, n2)) return false; - m_extensionality_todo.push_back(std::make_pair(n1, n2)); + m_extensionality_todo.push_back({n1, n2}); return true; } @@ -348,7 +346,7 @@ namespace smt { enode * nodes[2] = { a1, a2 }; if (!ctx.add_fingerprint(this, 1, 2, nodes)) return; // axiom was already instantiated - m_congruent_todo.push_back(std::make_pair(a1, a2)); + m_congruent_todo.push_back({a1, a2}); } @@ -536,6 +534,7 @@ namespace smt { unsigned num_vars = get_num_vars(); for (unsigned i = 0; i < num_vars; ++i) { enode * n = get_enode(i); + TRACE(array, tout << enode_pp(n, ctx) << " is_relevant: " << ctx.is_relevant(n) << " is_array: " << is_array_sort(n) << "\n";); if (!ctx.is_relevant(n) || !is_array_sort(n)) { continue; } @@ -581,11 +580,12 @@ namespace smt { enode * n2 = get_enode(v2); 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)) { + app_ref eq = app_ref(mk_eq_atom(n1->get_expr(), n2->get_expr()), m); + TRACE(array_bug, tout << "mk_interface_eqs: adding: " << eq << "\n";); + if (!ctx.b_internalized(eq.get()) || !ctx.is_relevant(eq.get())) { result++; ctx.internalize(eq, true); - ctx.mark_as_relevant(eq); + ctx.mark_as_relevant(eq.get()); } } } @@ -850,7 +850,7 @@ namespace smt { if (i < num_args) { SASSERT(!parent_sel_set->contains(sel) || (*(parent_sel_set->find(sel)))->get_root() == sel->get_root()); parent_sel_set->insert(sel); - todo.push_back(std::make_pair(parent_root, sel)); + todo.push_back({parent_root, sel}); } } } diff --git a/src/smt/theory_array_full.cpp b/src/smt/theory_array_full.cpp index 137359841e..e00f4cff93 100644 --- a/src/smt/theory_array_full.cpp +++ b/src/smt/theory_array_full.cpp @@ -393,6 +393,10 @@ namespace smt { SASSERT(is_map(map)); instantiate_select_map_axiom(s, map); } + for (enode *lam : d_full->m_lambdas) { + SASSERT(is_lambda(lam->get_expr())); + instantiate_select_lambda_axiom(s, lam); + } if (!m_params.m_array_delay_exp_axiom && d->m_prop_upward) { for (enode * map : d_full->m_parent_maps) { SASSERT(is_map(map)); @@ -468,7 +472,6 @@ namespace smt { SASSERT(map->get_num_args() > 0); func_decl* f = to_func_decl(map->get_decl()->get_parameter(0).get_ast()); - TRACE(array_map_bug, tout << "invoked instantiate_select_map_axiom\n"; tout << sl->get_owner_id() << " " << mp->get_owner_id() << "\n"; tout << mk_ismt2_pp(sl->get_expr(), m) << "\n" << mk_ismt2_pp(mp->get_expr(), m) << "\n";); @@ -518,6 +521,34 @@ namespace smt { return try_assign_eq(sel1, sel2); } + bool theory_array_full::instantiate_select_lambda_axiom(enode* sl, enode* lambda) { + app* select = sl->get_app(); + SASSERT(is_select(select)); + SASSERT(is_lambda(lambda->get_expr())); + SASSERT(lambda->get_sort() == sl->get_arg(0)->get_sort()); + + if (!ctx.add_fingerprint(lambda, lambda->get_owner_id(), sl->get_num_args() - 1, sl->get_args() + 1)) { + return false; + } + + m_stats.m_num_select_lambda_axiom++; + + unsigned num_args = select->get_num_args(); + ptr_buffer args; + args.push_back(lambda->get_expr()); + for (unsigned i = 1; i < num_args; ++i) + args.push_back(select->get_arg(i)); + + expr_ref sel1(m), sel2(m); + sel1 = mk_select(args.size(), args.data()); + sel2 = sel1; + ctx.get_rewriter()(sel2); + ctx.internalize(sel1, false); + ctx.internalize(sel2, false); + TRACE(array, tout << mk_bounded_pp(sel1, m) << "\n==\n" << mk_bounded_pp(sel2, m) << "\n";); + return try_assign_eq(sel1, sel2); + } + // // @@ -881,6 +912,7 @@ 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 sel lambda", m_stats.m_num_select_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 f66762e6b8..9dae2dcb96 100644 --- a/src/smt/theory_array_full.h +++ b/src/smt/theory_array_full.h @@ -83,7 +83,7 @@ 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); @@ -96,6 +96,7 @@ namespace smt { bool instantiate_select_const_axiom(enode* select, enode* cnst); bool instantiate_select_as_array_axiom(enode* select, enode* arr); bool instantiate_select_map_axiom(enode* select, enode* map); + bool instantiate_select_lambda_axiom(enode *select, enode *lambda); bool instantiate_axiom_map_for(theory_var v); diff --git a/src/smt/theory_finite_set.cpp b/src/smt/theory_finite_set.cpp index 899b4d1dec..dc1aae1756 100644 --- a/src/smt/theory_finite_set.cpp +++ b/src/smt/theory_finite_set.cpp @@ -437,11 +437,12 @@ namespace smt { return lit == arg; }; auto lit1 = clause.get(0); + [[maybe_unused]] auto lit2 = clause.get(1); auto position = 0; if (is_complement_to(is_true, lit1, e)) position = 0; else { - SASSERT(is_complement_to(is_true, clause.get(1), e)); + SASSERT(is_complement_to(is_true, lit2, e)); position = 1; } diff --git a/src/smt/theory_lra.cpp b/src/smt/theory_lra.cpp index b6ef6340a5..8d5a3178f9 100644 --- a/src/smt/theory_lra.cpp +++ b/src/smt/theory_lra.cpp @@ -4044,7 +4044,7 @@ public: if (!lp().is_feasible() || lp().has_changed_columns()) make_feasible(); vi = get_lpvar(v); - auto st = lp().maximize_term(vi, term_max); + auto st = lp().maximize_term(vi, term_max, /*fix_int_cols*/ true); if (has_int() && lp().has_inf_int()) { st = lp::lp_status::FEASIBLE; lp().restore_x(); diff --git a/src/smt/theory_nseq.cpp b/src/smt/theory_nseq.cpp index 71cf24dda5..0da43bd7f0 100644 --- a/src/smt/theory_nseq.cpp +++ b/src/smt/theory_nseq.cpp @@ -193,16 +193,16 @@ namespace smt { void theory_nseq::new_eq_eh(theory_var v1, theory_var v2) { try { - auto n1 = get_enode(v1); - auto n2 = get_enode(v2); - auto e1 = n1->get_expr(); - auto e2 = n2->get_expr(); + const auto n1 = get_enode(v1); + const auto n2 = get_enode(v2); + const auto e1 = n1->get_expr(); + const auto e2 = n2->get_expr(); TRACE(seq, tout << mk_pp(e1, m) << " == " << mk_pp(e2, m) << "\n"); //std::cout << mk_pp(e1, m) << " == " << mk_pp(e2, m) << std::endl; if (m_seq.is_re(e1)) { - expr_ref s(m); - auto r = m_rewriter.mk_symmetric_diff(e1, e2); - switch (m_rewriter.some_seq_in_re(r, s)) { + zstring s; + const auto r = m_rewriter.mk_symmetric_diff(e1, e2); + switch (m_rewriter.some_string_in_re(r, s)) { case l_false: // regexes are equivalent: nothing to do return; @@ -237,15 +237,15 @@ namespace smt { } void theory_nseq::new_diseq_eh(theory_var v1, theory_var v2) { - auto n1 = get_enode(v1); - auto n2 = get_enode(v2); - auto e1 = n1->get_expr(); - auto e2 = n2->get_expr(); + const auto n1 = get_enode(v1); + const auto n2 = get_enode(v2); + const auto e1 = n1->get_expr(); + const auto e2 = n2->get_expr(); TRACE(seq, tout << mk_pp(e1, m) << " != " << mk_pp(e2, m) << "\n"); if (m_seq.is_re(e1)) { - expr_ref s(m); - auto r = m_rewriter.mk_symmetric_diff(e1, e2); - switch (m_rewriter.some_seq_in_re(r, s)) { + zstring s; + auto r = m_rewriter.mk_symmetric_diff(e1, e2); + switch (m_rewriter.some_string_in_re(r, s)) { case l_false: { enode_pair_vector eqs; const auto lit = mk_eq(e1, e2, false); diff --git a/src/smt/theory_sls.h b/src/smt/theory_sls.h index 2b65783b4e..856f8f3d25 100644 --- a/src/smt/theory_sls.h +++ b/src/smt/theory_sls.h @@ -66,7 +66,6 @@ namespace smt { unsigned m_final_check_ls_steps = 30000; unsigned m_final_check_ls_steps_delta = 10000; unsigned m_final_check_ls_steps_min = 10000; - unsigned m_final_check_ls_steps_max = 30000; bool m_has_unassigned_clause_after_resolve = false; unsigned m_after_resolve_decide_gap = 4; unsigned m_after_resolve_decide_count = 0; diff --git a/src/solver/CMakeLists.txt b/src/solver/CMakeLists.txt index 9c6ba9d6e3..86316c86e5 100644 --- a/src/solver/CMakeLists.txt +++ b/src/solver/CMakeLists.txt @@ -5,7 +5,6 @@ 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/assertions/asserted_formulas.h b/src/solver/assertions/asserted_formulas.h index 262499ff44..ba0b1f8406 100644 --- a/src/solver/assertions/asserted_formulas.h +++ b/src/solver/assertions/asserted_formulas.h @@ -185,12 +185,12 @@ class asserted_formulas { public: \ FUNCTOR m_functor; \ NAME(asserted_formulas& af):simplify_fmls(af, MSG), m_functor ARG {} \ - virtual void simplify(justified_expr const& j, expr_ref& n, proof_ref& p) { \ - m_functor(j.fml(), n, p); \ + void simplify(justified_expr const& j, expr_ref& n, proof_ref& p) override { \ + m_functor(j.fml(), n, p); \ } \ - virtual void post_op() { if (REDUCE) af.reduce_and_solve(); } \ - virtual bool should_apply() const { return APP; } \ - }; \ + void post_op() override { if (REDUCE) af.reduce_and_solve(); } \ + bool should_apply() const override { return APP; } \ + }; #define MK_SIMPLIFIERF(NAME, FUNCTOR, MSG, APP, REDUCE) MK_SIMPLIFIERA(NAME, FUNCTOR, MSG, APP, (af.m), REDUCE) diff --git a/src/solver/parallel_params.pyg b/src/solver/parallel_params.pyg index 2aa2acf776..62fd503fe2 100644 --- a/src/solver/parallel_params.pyg +++ b/src/solver/parallel_params.pyg @@ -4,8 +4,11 @@ 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'), + ('num_bb_threads', UINT, 2, 'run Janota-style chunking backbone worker threads; default is 2 (negative and positive mode), supported values are 0 (off), 1 (negative mode only) or 2 (negative and positive mode)'), + ('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'), + ('cube.lookahead', BOOL, False, 'use lookahead cubing in the parallel solver; when false, use VSIDS activity to select one split literal'), ('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'), ('conquer.delay', UINT, 10, 'delay of cubes until applying conquer'), diff --git a/src/solver/parallel_tactical.cpp b/src/solver/parallel_tactical.cpp index 068dc03043..972379ea7c 100644 --- a/src/solver/parallel_tactical.cpp +++ b/src/solver/parallel_tactical.cpp @@ -1,878 +1,2217 @@ /*++ -Copyright (c) 2017 Microsoft Corporation +Copyright (c) 2024 Microsoft Corporation Module Name: - parallel_tactical.cpp + parallel_tactical2.cpp Abstract: - Parallel tactic based on cubing. - + Parallel tactical, portfolio loop specialized to the solver API. Author: - Nikolaj Bjorner (nbjorner) 2017-10-9 - Miguel Neves + (based on smt_parallel.cpp by nbjorner / Ilana Shapiro, and + parallel_tactical.cpp by nbjorner / Miguel Neves) -Notes: - - A task comprises of a non-empty sequence of cubes, a type and parameters - - It invokes the following procedure: - 1. Clone the state with the remaining cubes if there is more than one cube. Re-enqueue the remaining cubes. - 2. Apply simplifications and pre-processing according to configuration. - 3. Cube using the parameter settings prescribed in m_params. - 4. Optionally pass the cubes as assumptions and solve each sub-cube with a prescribed resource bound. - 5. Assemble cubes that could not be solved and create a cube state. - --*/ #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/solver2tactic.h" -#include "tactic/tactic.h" -#include "tactic/tactical.h" #include "solver/parallel_tactical.h" #include "solver/parallel_params.hpp" +#include "util/search_tree.h" +#include "tactic/tactic.h" +#include "solver/solver2tactic.h" - -class non_parallel_tactic : public tactic { -public: - non_parallel_tactic(solver* s, params_ref const& p) { - } - - char const* name() const override { return "parallel_tactic"; } - - void operator()(const goal_ref & g,goal_ref_buffer & result) override { - throw default_exception("parallel tactic is disabled in single threaded mode"); - } - tactic * translate(ast_manager & m) override { return nullptr; } - void cleanup() override {} - -}; +#include +#include +#include +#include +#include #ifdef SINGLE_THREAD -tactic * mk_parallel_tactic(solver* s, params_ref const& p) { - return alloc(non_parallel_tactic, s, p); +tactic* mk_parallel_tactic(solver* s, params_ref const& /* p */) { + return mk_solver2tactic(s); } #else #include #include -#include -#include #include -class parallel_tactic : public tactic { +#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) +class bounded_pp_exprs { + expr_ref_vector const &es; - class solver_state; +public: + bounded_pp_exprs(expr_ref_vector const &es) : es(es) {} - class task_queue { - std::mutex m_mutex; - std::condition_variable m_cond; - ptr_vector m_tasks; - ptr_vector m_active; - unsigned m_num_waiters; - std::atomic m_shutdown; + std::ostream &display(std::ostream &out) const { + for (auto e : es) + out << mk_bounded_pp(e, es.get_manager()) << "\n"; + return out; + } +}; - void inc_wait() { - std::lock_guard lock(m_mutex); - ++m_num_waiters; - } +inline std::ostream &operator<<(std::ostream &out, bounded_pp_exprs const &pp) { + return pp.display(out); +} - void dec_wait() { - std::lock_guard lock(m_mutex); - --m_num_waiters; - } +/* ------------------------------------------------------------------ */ +/* Search-tree literal configuration */ +/* ------------------------------------------------------------------ */ - solver_state* try_get_task() { - solver_state* st = nullptr; - std::lock_guard lock(m_mutex); - if (!m_tasks.empty()) { - st = m_tasks.back(); - m_tasks.pop_back(); - m_active.push_back(st); +struct solver_cube_config { + using literal = expr_ref; + static bool literal_is_null(expr_ref const& l) { return l == nullptr; } + static bool same_atom(expr_ref const& a, expr_ref const& b) { + expr* atom_a = a.get(); + expr* atom_b = b.get(); + a.get_manager().is_not(atom_a, atom_a); + b.get_manager().is_not(atom_b, atom_b); + return atom_a == atom_b; + } + 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)"; + } +}; + +static bool is_cancellation_exception(char const* msg) { + return msg && (strstr(msg, "canceled") != nullptr || strstr(msg, "cancelled") != nullptr); +} + +/* ------------------------------------------------------------------ */ +/* parallel_solver – the core portfolio engine */ +/* ------------------------------------------------------------------ */ + +class parallel_solver { + + /* ---- forward declarations ---- */ + class worker; + class core_minimizer_worker; + class backbones_worker; + + /* ---- node lease (mirrors smt_parallel) ---- */ + struct node_lease { + search_tree::node* leased_node = nullptr; + + // 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; + }; + + /* ---- shared clause entry ---- */ + 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; + + class batch_manager { + + enum state { + is_running, + is_sat, + is_unsat, + is_unknown, + is_exception_msg, + is_exception_code + }; + + struct stats { + unsigned m_num_cubes = 0; + unsigned m_max_cube_depth = 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; + unsigned m_bb_candidates_enqueued = 0; + unsigned m_bb_batches_issued = 0; + }; + + struct core_min_job { + search_tree::node* source = nullptr; + expr_ref_vector core; + core_min_job(ast_manager& m, search_tree::node* source) + : source(source), core(m) {} + }; + + 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; + + vector m_shared_clause_trail; // store all shared clauses with worker IDs + obj_hashtable m_shared_clause_set; // for duplicate filtering on per-thread clause expressions + + obj_hashtable m_global_backbones; + + bb_candidates m_bb_candidates; + unsigned m_max_global_bb_candidates = 100; + unsigned m_bb_batch_size = 150; + std::atomic m_bb_candidate_epoch = 0; + std::condition_variable m_bb_cv; + bb_candidates m_bb_current_batch; + unsigned m_bb_batch_id = 0; + unsigned m_num_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; + + unsigned m_exception_code = 0; + std::string m_exception_msg; + std::string m_reason_unknown; + model_ref m_model; + expr_ref_vector m_unsat_core; + std::atomic m_canceled = false; + + // called from batch manager to cancel other workers if we've reached a verdict + void cancel_background_threads() { + if (m_canceled.exchange(true)) + return; + IF_VERBOSE(1, verbose_stream() << "Canceling workers\n"); + for (auto* w : p.m_workers) + w->cancel(); + if (p.m_core_minimizer_worker) { + p.m_core_minimizer_worker->cancel(); + m_core_min_cv.notify_all(); } - return st; + if (!p.m_global_backbones_workers.empty()) + IF_VERBOSE(1, verbose_stream() << "Canceling backbones workers\n"); + for (auto* w : p.m_global_backbones_workers) + w->cancel(); + if (!p.m_global_backbones_workers.empty()) + m_bb_cv.notify_all(); } - public: + void set_canceled_unlocked() { + if (m_state != state::is_running) + return; + cancel_background_threads(); + } - task_queue(): - m_num_waiters(0), - m_shutdown(false) {} + void release_worker_lease_unlocked(unsigned worker_id, node_lease& lease) { + if (worker_id >= m_worker_leases.size()) { + lease = {}; + return; + } + auto& stored_lease = m_worker_leases[worker_id]; + if (!stored_lease.leased_node || stored_lease.leased_node != lease.leased_node) { + lease = {}; + return; + } + bool cancel_signaled = stored_lease.cancel_signaled; + m_search_tree.dec_active_workers(stored_lease.leased_node); + stored_lease = {}; + lease = {}; + if (cancel_signaled) + p.m_workers[worker_id]->limit().dec_cancel(); + } - ~task_queue() { reset(); } + bool attempt_release_canceled_lease_unlocked(unsigned worker_id, node_lease& lease) { + if (m_state != state::is_running || !lease.leased_node || worker_id >= m_worker_leases.size()) + return false; - void shutdown() { - if (!m_shutdown) { - std::lock_guard lock(m_mutex); - m_shutdown = true; - m_cond.notify_all(); - for (solver_state* st : m_active) { - st->m().limit().cancel(); + auto& stored_lease = m_worker_leases[worker_id]; + if (stored_lease.leased_node != lease.leased_node) + return false; + + if (!m_search_tree.is_lease_canceled(stored_lease.leased_node)) + return false; + + release_worker_lease_unlocked(worker_id, lease); + return true; + } + + 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)) { + m_worker_leases[id].cancel_signaled = true; + p.m_workers[id]->cancel_lease(); } } } - bool in_shutdown() const { return m_shutdown; } - - void add_task(solver_state* task) { - std::lock_guard lock(m_mutex); - m_tasks.push_back(task); - if (m_num_waiters > 0) { - m_cond.notify_one(); - } - } - - bool is_idle() { - std::lock_guard lock(m_mutex); - return m_tasks.empty() && m_num_waiters > 0; + 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)); + } } - solver_state* get_task() { - while (!m_shutdown) { - inc_wait(); - solver_state* st = try_get_task(); - if (st) { - dec_wait(); - return st; + // to avoid deadlock + 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()); + } + + bool is_global_backbone_or_negation_unlocked(ast_translation& l2g, expr* bb_cand) { + expr_ref cand(l2g(bb_cand), m); + expr_ref neg_cand(mk_not(m, cand), m); + return m_global_backbones.contains(cand.get()) || + m_global_backbones.contains(neg_cand.get()); + } + + void collect_matching_targets_unlocked(search_tree::node* source, + expr* lit, + vector const& core, + vector& targets) { + targets.reset(); + if (!lit) + return; + + auto is_ancestor_of = [&](search_tree::node* ancestor, + search_tree::node* cur) { + if (!ancestor) + return false; + for (auto* p = cur; p; p = p->parent()) { + if (p == ancestor) + return true; } - { - std::unique_lock lock(m_mutex); - if (!m_shutdown) { - m_cond.wait(lock); + return false; + }; + + auto path_contains = [&](search_tree::node* cur, + solver_cube_config::literal const& lit0) { + for (auto* p = cur; p; p = p->parent()) { + if (p->get_literal() == lit0) + return true; + } + return false; + }; + + auto path_contains_core = [&](search_tree::node* cur) { + return all_of(core, [&](solver_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 (auto* t : matches) { + if (!t || t == source) + continue; + if (m_search_tree.is_lease_canceled(t)) + 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 (auto* 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; } } - dec_wait(); + if (!is_highest_ancestor) + continue; + + targets.push_back({ t }); + } + } + + // 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 + search_tree::node* find_core_source_unlocked( + ast_translation& l2g, search_tree::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 (auto* cur = source; cur; cur = cur->parent()) { + if (solver_cube_config::literal_is_null(cur->get_literal())) + continue; + if (any_of(g_core, [&](solver_cube_config::literal const& lit) { return lit == cur->get_literal(); })) + return cur; } return nullptr; } - void task_done(solver_state* st) { - std::lock_guard lock(m_mutex); - m_active.erase(st); - if (m_tasks.empty() && m_active.empty()) { - m_shutdown = true; - m_cond.notify_all(); + unsigned select_best_core_min_job_unlocked() const { + SASSERT(!m_core_min_jobs.empty()); + unsigned best_idx = 0; + auto* 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) { + auto* job = m_core_min_jobs[i]; + auto* 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 backtrack_unlocked(ast_translation& l2g, unsigned worker_id, + expr_ref_vector const& core, + node_lease* lease = nullptr, + vector const* targets = nullptr) { + if (m_state != state::is_running) + return; + + vector g_core; + for (expr* c : core) + g_core.push_back(expr_ref(l2g(c), m)); + + SASSERT(lease != nullptr || targets != nullptr); + bool did_backtrack = false; + + if (lease) { + if (!m_search_tree.is_lease_canceled(lease->leased_node)) { + // 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"); + auto* leased_node = lease->leased_node; + release_worker_lease_unlocked(worker_id, *lease); + m_search_tree.backtrack(leased_node, g_core); + } + else { + // the lease was canceled by another worker. don't backtrack on this node with the new, arbitrary core we just found with this thread + // however, we do proceed to external targets, since the new code may have exposed new external targets we can close/backtrack + attempt_release_canceled_lease_unlocked(worker_id, *lease); + } + } + if (targets) { + for (auto const& target : *targets) { + if (m_search_tree.is_lease_canceled(target.leased_node)) + continue; + did_backtrack = true; + 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, + for (expr* e : core) + verbose_stream() << mk_bounded_pp(e, core.get_manager()) << "\n"; + m_search_tree.display(verbose_stream() << "\n"); + ); + + if (m_search_tree.is_closed()) { + IF_VERBOSE(1, verbose_stream() << "Search tree closed, setting UNSAT\n"); + m_state = state::is_unsat; + SASSERT(m_unsat_core.empty()); + for (auto& e : m_search_tree.get_core_from_root()) + m_unsat_core.push_back(e.get()); + cancel_background_threads(); } } - void stats(::statistics& st) { - for (auto* t : m_tasks) - t->get_solver().collect_statistics(st); - for (auto* t : m_active) - t->get_solver().collect_statistics(st); - } - - void reset() { - for (auto* t : m_tasks) - dealloc(t); - for (auto* t : m_active) - dealloc(t); - m_tasks.reset(); - m_active.reset(); - m_num_waiters = 0; - m_shutdown = false; - } - - std::ostream& display(std::ostream& out) { - std::lock_guard lock(m_mutex); - out << "num_tasks " << m_tasks.size() << " active: " << m_active.size() << "\n"; - for (solver_state* st : m_tasks) { - st->display(out); - } - return out; - } - - }; - - class cube_var { - expr_ref_vector m_vars; - expr_ref_vector m_cube; public: - cube_var(expr_ref_vector const& c, expr_ref_vector const& vs): - m_vars(vs), m_cube(c) {} - cube_var operator()(ast_translation& tr) { - expr_ref_vector vars(tr(m_vars)); - expr_ref_vector cube(tr(m_cube)); - return cube_var(cube, vars); + batch_manager(ast_manager& m, parallel_solver& p) + : m(m), p(p), + m_search_tree(expr_ref(m)), + m_unsat_core(m) {} + + void initialize(unsigned num_bb_threads, 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(p.m_workers.size()); + m_shared_clause_trail.reset(); + m_shared_clause_set.reset(); + m_global_backbones.reset(); + m_bb_candidates.reset(); + m_bb_current_batch.reset(); + m_bb_batch_id = 0; + m_num_bb_threads = num_bb_threads; + m_bb_last_batch_processed.reset(); + m_bb_last_batch_processed.resize(num_bb_threads); + m_bb_cancel_epoch = 0; + m_bb_candidate_epoch.store(0, std::memory_order_release); + m_core_min_jobs.reset(); + m_model = nullptr; + m_unsat_core.reset(); + m_reason_unknown.clear(); + m_canceled = false; } - expr_ref_vector const& cube() const { return m_cube; } - expr_ref_vector const& vars() const { return m_vars; } - }; - - class solver_state { - scoped_ptr m_manager; // ownership handle to ast_manager - vector m_cubes; // set of cubes to process by task - expr_ref_vector m_asserted_cubes; // set of cubes asserted on the current solver - expr_ref_vector m_assumptions; // set of auxiliary assumptions passed in - params_ref m_params; // configuration parameters - ref m_solver; // solver state - unsigned m_depth; // number of nested calls to cubing - double m_width; // estimate of fraction of problem handled by state - bool m_giveup; - - public: - solver_state(ast_manager* m, solver* s, params_ref const& p): - m_manager(m), - m_asserted_cubes(s->get_manager()), - m_assumptions(s->get_manager()), - m_params(p), - m_solver(s), - m_depth(0), - m_width(1.0), - m_giveup(false) - { + void set_sat(ast_translation& l2g, model& mdl) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "Batch manager setting SAT.\n"); + if (m_state != state::is_running && m_state != state::is_unknown) return; + m_state = state::is_sat; + m_model = mdl.translate(l2g); + cancel_background_threads(); } - ast_manager& m() { return m_solver->get_manager(); } - - solver& get_solver() { return *m_solver; } - - solver* copy_solver() { return m_solver->translate(m_solver->get_manager(), m_params); } - - solver const& get_solver() const { return *m_solver; } - - void set_assumptions(ptr_vector const& asms) { - m_assumptions.append(asms.size(), asms.data()); + void set_canceled() { + std::scoped_lock lock(mux); + set_canceled_unlocked(); } - bool has_assumptions() const { return !m_assumptions.empty(); } - - solver_state* clone() { - SASSERT(!m_cubes.empty()); - ast_manager& m = m_solver->get_manager(); - ast_manager* new_m = alloc(ast_manager, m, true); - ast_translation tr(m, *new_m); - solver* s = m_solver.get()->translate(*new_m, m_params); - solver_state* st = alloc(solver_state, new_m, s, m_params); - for (auto& c : m_cubes) st->m_cubes.push_back(c(tr)); - for (expr* c : m_asserted_cubes) st->m_asserted_cubes.push_back(tr(c)); - for (expr* c : m_assumptions) st->m_assumptions.push_back(tr(c)); - st->m_depth = m_depth; - st->m_width = m_width; - return st; + void set_unsat(ast_translation& l2g, + expr_ref_vector const& core) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "Batch manager setting UNSAT.\n"); + if (m_state != state::is_running && m_state != state::is_unknown) return; + m_state = state::is_unsat; + m_unsat_core.reset(); + for (expr* c : core) + m_unsat_core.push_back(l2g(c)); + cancel_background_threads(); } - vector const& cubes() const { return m_cubes; } + // A worker found a cube whose result is genuinely undetermined (e.g. the + // theory solver is incomplete) and that cannot be refined any further. + // Record a sound 'unknown' verdict. This is a soft result: it does not + // cancel the remaining workers, so a definitive sat/unsat verdict from + // another branch may still arrive and override it. + // Backbone workers and the core minimizer block on their own condition + // variables waiting for work that will never arrive once the state + // transitions away from is_running, so we must notify them here to + // prevent a deadlock. + void set_unknown(std::string const& reason) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "Batch manager setting UNKNOWN: " << reason << ".\n"); + if (m_state != state::is_running) return; + m_state = state::is_unknown; + m_reason_unknown = reason; + m_bb_cv.notify_all(); + m_core_min_cv.notify_all(); + } - // remove up to n cubes from list of cubes. - vector split_cubes(unsigned n) { - vector result; - while (n-- > 0 && !m_cubes.empty()) { - DEBUG_CODE(for (expr* c : m_cubes.back().cube()) SASSERT(c);); - result.push_back(m_cubes.back()); + void set_exception(std::string const& msg) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "Batch manager setting exception msg: " << msg << ".\n"); + if (m_state != state::is_running) return; + m_state = state::is_exception_msg; + m_exception_msg = msg; + cancel_background_threads(); + } - m_cubes.pop_back(); + void set_exception(unsigned error_code) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "Batch manager setting exception code: " << error_code << ".\n"); + if (m_state != state::is_running) return; + m_state = state::is_exception_code; + m_exception_code = error_code; + cancel_background_threads(); + } + + 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; + if (id >= m_worker_leases.size()) + m_worker_leases.resize(id + 1); + m_worker_leases[id] = lease; + + 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; + } + + void backtrack(ast_translation& l2g, unsigned worker_id, + expr_ref_vector const& core, + node_lease& lease) { + std::scoped_lock lock(mux); + if (m_state != state::is_running) return; + vector g_core; + for (expr* c : core) + g_core.push_back(expr_ref(l2g(c), m)); + + vector targets; + expr* lit = lease.leased_node ? lease.leased_node->get_literal().get() : nullptr; + collect_matching_targets_unlocked(lease.leased_node, lit, g_core, targets); + backtrack_unlocked(l2g, worker_id, core, &lease, targets.empty() ? nullptr : &targets); + } + + void enqueue_core_minimization(ast_translation& l2g, + search_tree::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 wait_for_core_min_job(ast_translation& g2l, + search_tree::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); + scoped_ptr 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)); + return source != nullptr; + } + + void publish_minimized_core(ast_translation& l2g, + expr_ref_vector const& asms, + search_tree::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 resolution propagation) + if (source->get_core().size() <= g_core.size()) { + ++m_stats.m_core_min_jobs_skipped; + return; + } + + if (all_of(g_core, [&](solver_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(m_unsat_core.empty()); + for (expr* e : minimized_core) + 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)) + m_search_tree.backtrack(target.leased_node, g_core); + } + } + + ++m_stats.m_core_min_jobs_published; + cancel_closed_leases_unlocked(UINT_MAX); + + IF_VERBOSE(1, verbose_stream() << "Batch manager publishing minimized core " + << original_core_size << " -> " << minimized_core.size() << "\n"); + IF_VERBOSE(2, + for (expr* e : minimized_core) + verbose_stream() << mk_bounded_pp(e, minimized_core.get_manager()) << "\n"; + m_search_tree.display(verbose_stream() << "\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(m_unsat_core.empty()); + for (auto& e : m_search_tree.get_core_from_root()) + m_unsat_core.push_back(e.get()); + cancel_background_threads(); + } + } + + bool path_contains_atom(ast_translation& l2g, node_lease const& lease, expr* atom) { + std::scoped_lock lock(mux); + if (!lease.leased_node) + return false; + if (m_state != state::is_running) + return false; + if (m_search_tree.is_lease_canceled(lease.leased_node)) + return false; + + expr_ref _atom(l2g(atom), m); + return lease.leased_node->path_contains_atom(_atom); + } + + void try_split(ast_translation& l2g, unsigned worker_id, + node_lease& 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)) { + attempt_release_canceled_lease_unlocked(worker_id, lease); + return; + } + + expr_ref lit(m), nlit(m); + auto* leased_node = lease.leased_node; + lit = l2g(atom); + nlit = mk_not(m, lit); + VERIFY(!leased_node->path_contains_atom(lit)); + VERIFY(!leased_node->path_contains_atom(nlit)); + + bool did_split = m_search_tree.try_split(leased_node, lit, nlit, effort); + + release_worker_lease_unlocked(worker_id, lease); + + if (did_split) { + ++m_stats.m_num_cubes; + m_stats.m_max_cube_depth = std::max( + m_stats.m_max_cube_depth, + leased_node->depth() + 1); + IF_VERBOSE(1, verbose_stream() << "Batch manager splitting on literal: " << mk_bounded_pp(lit, m, 3) << "\n"); + } + } + + bool attempt_release_canceled_lease(unsigned worker_id, node_lease& lease) { + std::scoped_lock lock(mux); + return attempt_release_canceled_lease_unlocked(worker_id, lease); + } + + bool checkpoint_worker(unsigned worker_id, node_lease& lease, bool& lease_canceled) { + std::scoped_lock lock(mux); + lease_canceled = false; + SASSERT(worker_id < p.m_workers.size()); + + if (attempt_release_canceled_lease_unlocked(worker_id, lease)) { + lease_canceled = true; + return true; + } + + if (p.m_workers[worker_id]->limit().inc()) + return true; + + if (attempt_release_canceled_lease_unlocked(worker_id, lease)) { + lease_canceled = true; + return true; + } + + set_canceled_unlocked(); + return false; + } + + 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(); // update the worker limit to the end of the current trail + return result; + } + + bool collect_global_backbone(ast_translation& l2g, expr_ref const& backbone, unsigned source_worker_id = UINT_MAX) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "collect-global-backbone\n"); + 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(1, verbose_stream() << " Found and sharing new global backbone: " << mk_bounded_pp(g_bb, m, 3) << "\n"); + collect_clause_unlocked(l2g, source_worker_id, backbone.get()); + + expr_ref neg_g_bb(mk_not(m, g_bb), m); + vector g_core; + g_core.push_back(neg_g_bb); + vector targets; + collect_matching_targets_unlocked(nullptr, neg_g_bb, g_core, targets); + + if (!targets.empty()) { + IF_VERBOSE(1, verbose_stream() << " Closing negation of the new global backbone: " + << mk_bounded_pp(g_bb, m, 3) << "\n"); + 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; + } + + 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); + } + + 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); + } + + 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; + } + + 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; + } + + void 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.get())) + 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 incoming(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(incoming) > rank_of(*worst_it)) { + *worst_it = incoming; + changed = true; + } + } + + if (changed && !m_bb_candidates.empty()) { + m_bb_candidate_epoch.fetch_add(1, std::memory_order_release); + std::stable_sort( + m_bb_candidates.begin(), + m_bb_candidates.end(), + [&](bb_candidate const& a, bb_candidate const& b) { + return rank_of(a) < rank_of(b); + }); + m_bb_cv.notify_all(); + } + } + + bool wait_for_backbone_job(unsigned bb_thread_id, ast_translation& g2l, + bb_candidates& out, reslimit& lim) { + out.reset(); + std::unique_lock lock(mux); + 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() || m_state != state::is_running) + return false; + + if (m_bb_last_batch_processed[bb_thread_id] == m_bb_batch_id) { + 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; + 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; + } + + lbool get_result() const { + if (m.limit().is_canceled()) + return l_undef; // the main context was cancelled, so we return undef. + switch (m_state) { + case state::is_running: // batch manager is still running, but all threads have processed their cubes, which + // means all cubes were unsat + throw default_exception("par2: inconsistent end state"); + case state::is_sat: return l_true; + case state::is_unsat: return l_false; + case state::is_unknown: return l_undef; + 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; + } + } + + std::string const& get_reason_unknown() const { return m_reason_unknown; } + + 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("parallel-num-cubes", m_stats.m_num_cubes); + st.update("parallel-max-cube-size", m_stats.m_max_cube_depth); + st.update("parallel-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); + } + }; + + /* ================================================================ + * 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; + bool m_share_units_relevant_only = true; + bool m_share_units_initial_only = true; + bool m_core_minimize = false; + bool m_global_backbones = false; + bool m_ablate_backtracking = false; + unsigned m_max_cube_depth = 20; + }; + + unsigned id; + 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; + uint_set m_known_units; /* bool vars already shared by this worker */ + unsigned m_shared_clause_limit = 0; + unsigned m_shared_units_prefix = 0; + unsigned m_num_initial_atoms = 0; + + void update_max_thread_conflicts() { + m_config.m_threads_max_conflicts = static_cast( + m_config.m_max_conflict_mul * m_config.m_threads_max_conflicts); + } + + lbool check_cube(expr_ref_vector const& cube) { + s->set_max_conflicts(std::min(m_config.m_threads_max_conflicts, m_config.m_max_conflicts)); + + expr_ref_vector combined(m); + combined.append(asms); + combined.append(cube); + + IF_VERBOSE(1, verbose_stream() << " Checking cube\n" + << bounded_pp_exprs(cube) + << "with max_conflicts: " + << std::min(m_config.m_threads_max_conflicts, m_config.m_max_conflicts) + << "\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() && !is_cancellation_exception(ex.what())) + b.set_exception(ex.what()); + } + catch (...) { + if (!m.limit().is_canceled()) + b.set_exception("unknown exception"); + } + LOG_WORKER(1, " DONE checking cube " << r << "\n";); + return r; + } + + void collect_shared_clauses() { + expr_ref_vector nc = b.return_shared_clauses(m_g2l, m_shared_clause_limit, id); + for (expr* e : nc) { + LOG_WORKER(4, " asserting shared clause: " << mk_bounded_pp(e, m, 3) << "\n"); + s->assert_expr(e); + } + } + + void share_units() { + if (!m_config.m_share_units) + return; + expr_ref_vector trail = s->get_assigned_literals(); + unsigned sz = trail.size(); + unsigned prefix_sz = std::min(m_shared_units_prefix, sz); + bool at_prefix = true; + + for (unsigned i = m_shared_units_prefix; i < sz; ++i) { + expr* e = trail.get(i); + if (s->get_assign_level(e) > 0) { + at_prefix = false; + continue; + } + + if (at_prefix) + ++prefix_sz; + + expr* atom = e; + m.is_not(e, atom); + + if (m.is_and(atom) || m.is_or(atom) || m.is_ite(atom) || m.is_iff(atom)) + continue; + + unsigned v = s->get_bool_var(atom); + if (v == UINT_MAX) + continue; + if (m_known_units.contains(v)) + continue; + m_known_units.insert(v); + + if (m_config.m_share_units_relevant_only && !s->is_relevant(atom)) + continue; + if (m_config.m_share_units_initial_only && v >= m_num_initial_atoms) + continue; + + expr_ref lit(e, m); + b.collect_global_backbone(m_l2g, lit, id); + } + m_shared_units_prefix = prefix_sz; + } + + expr_ref get_split_atom(node_lease const& lease, expr_ref_vector const& cube) { + expr_ref result(m); + if (cube.size() >= m_config.m_max_cube_depth) + return result; + + obj_hashtable invalid_split_atoms_set; + expr_ref_vector invalid_split_atoms(m); + auto mark_invalid_split_atom = [&](expr* lit) { + expr* atom = lit; + m.is_not(lit, atom); + if (!invalid_split_atoms_set.contains(atom)) { + invalid_split_atoms_set.insert(atom); + invalid_split_atoms.push_back(atom); + } + }; + for (expr* lit : cube) { // don't split on atoms already in the cube + mark_invalid_split_atom(lit); + } + if (m_config.m_global_backbones) { // don't split on global backbones or their negations + expr_ref_vector global_backbones = b.get_global_backbones_snapshot(m_g2l); + for (expr* lit : global_backbones) { + mark_invalid_split_atom(lit); + } + } + + try { + return s->cube_vsids(invalid_split_atoms); + } + catch (...) { + if (!m.limit().is_canceled()) + b.set_exception("unknown exception"); + return result; } return result; } - void set_cubes(vector& c) { - m_cubes.reset(); - DEBUG_CODE(for (auto & cb : c) for (expr* e : cb.cube()) SASSERT(e);); - m_cubes.append(c); - } - - void inc_depth(unsigned inc) { m_depth += inc; } - - void inc_width(unsigned w) { m_width *= w; } - - double get_width() const { return m_width; } - - unsigned get_depth() const { return m_depth; } - - lbool simplify() { - lbool r = l_undef; - IF_VERBOSE(2, verbose_stream() << "(parallel.tactic simplify-1)\n";); - set_simplify_params(true); // retain blocked - r = get_solver().check_sat(m_assumptions); - if (r != l_undef) return r; - if (canceled()) return l_undef; - IF_VERBOSE(2, verbose_stream() << "(parallel.tactic simplify-2)\n";); - set_simplify_params(false); // remove blocked - r = get_solver().check_sat(m_assumptions); - return r; - } - - bool giveup() { - if (m_giveup) - return m_giveup; - std::string r = get_solver().reason_unknown(); - std::string inc("(incomplete"); - m_giveup |= r.compare(0, inc.size(), inc) == 0; - inc = "(sat.giveup"; - m_giveup |= r.compare(0, inc.size(), inc) == 0; - if (m_giveup) - IF_VERBOSE(0, verbose_stream() << r << "\n"); - return m_giveup; - } - - void assert_cube(expr_ref_vector const& cube) { - IF_VERBOSE(3, verbose_stream() << "assert cube: " << cube << "\n"); - get_solver().assert_expr(cube); - m_asserted_cubes.append(cube); - } - - void set_cube_params() { - } - - void set_conquer_params() { - set_conquer_params(get_solver()); - } - - void set_conquer_params(solver& s) { - parallel_params pp(m_params); - params_ref p; - p.copy(m_params); - p.set_bool("gc.burst", true); // apply eager gc - p.set_uint("simplify.delay", 1000); // delay simplification by 1000 conflicts - p.set_bool("lookahead_simplify", false); - p.set_uint("restart.max", pp.conquer_restart_max()); - p.set_uint("inprocess.max", UINT_MAX); // base bounds on restart.max - s.updt_params(p); - } - - void set_simplify_params(bool retain_blocked) { - parallel_params pp(m_params); - params_ref p; - p.copy(m_params); - double exp = pp.simplify_exp(); - exp = std::max(exp, 1.0); - unsigned mult = static_cast(pow(exp, static_cast(m_depth - 1))); - unsigned max_conflicts = pp.simplify_max_conflicts(); - if (max_conflicts < 1000000) - max_conflicts *= std::max(m_depth, 1u); - p.set_uint("inprocess.max", pp.simplify_inprocess_max() * mult); - p.set_uint("restart.max", pp.simplify_restart_max() * mult); - p.set_bool("lookahead_simplify", m_depth > 2); - p.set_bool("retain_blocked_clauses", retain_blocked); - p.set_uint("max_conflicts", max_conflicts); - if (m_depth > 1) p.set_uint("bce_delay", 0); - get_solver().updt_params(p); - } - - bool canceled() { - return m_giveup || !m().inc(); - } - - std::ostream& display(std::ostream& out) { - out << ":depth " << m_depth << " :width " << m_width << "\n"; - out << ":asserted " << m_asserted_cubes.size() << "\n"; - return out; - } - }; - -private: - - solver_ref m_solver; - ast_manager& m_manager; - scoped_ptr m_serialize_manager; - params_ref m_params; - sref_vector m_models; - scoped_ptr m_core; - unsigned m_num_threads; - statistics m_stats; - task_queue m_queue; - std::mutex m_mutex; - double m_progress; - unsigned m_branches; - unsigned m_backtrack_frequency; - unsigned m_conquer_delay; - std::atomic m_has_undef; - bool m_allsat; - unsigned m_num_unsat; - unsigned m_last_depth; - int m_exn_code; - std::string m_exn_msg; - std::string m_reason_undef; - - void init() { - parallel_params pp(m_params); - m_num_threads = std::min((unsigned) std::thread::hardware_concurrency(), pp.threads_max()); - m_progress = 0; - m_has_undef = false; - m_allsat = false; - m_branches = 0; - m_num_unsat = 0; - m_last_depth = 0; - m_backtrack_frequency = pp.conquer_backtrack_frequency(); - m_conquer_delay = pp.conquer_delay(); - m_exn_code = 0; - m_params.set_bool("override_incremental", true); - m_core = nullptr; - } - - void log_branches(lbool status) { - IF_VERBOSE(1, verbose_stream() << "(tactic.parallel :progress " << m_progress << "%"; - if (status == l_true) verbose_stream() << " :status sat"; - if (status == l_undef) verbose_stream() << " :status unknown"; - if (m_num_unsat > 0) verbose_stream() << " :closed " << m_num_unsat << "@" << m_last_depth; - verbose_stream() << " :open " << m_branches << ")\n";); - } - - void add_branches(unsigned b) { - if (b == 0) return; - { - std::lock_guard lock(m_mutex); - m_branches += b; - } - log_branches(l_false); - } - - void dec_branch() { - std::lock_guard lock(m_mutex); - --m_branches; - } - - void collect_core(expr_ref_vector const& core) { - std::lock_guard lock(m_mutex); - if (!m_serialize_manager) - m_serialize_manager = alloc(ast_manager, core.get_manager(), true); - m_core = nullptr; - m_core = alloc(expr_ref_vector, *m_serialize_manager); - ast_translation tr(core.get_manager(), *m_serialize_manager); - expr_ref_vector core1(tr(core)); - for (expr* c : core1) { - if (!m_core->contains(c)) - m_core->push_back(c); - } - } - - void close_branch(solver_state& s, lbool status) { - double f = 100.0 / s.get_width(); - { - std::lock_guard lock(m_mutex); - m_progress += f; - --m_branches; - } - log_branches(status); - } - - void report_sat(solver_state& s, solver* conquer) { - close_branch(s, l_true); - model_ref mdl; - if (conquer) { - conquer->get_model(mdl); - } - else { - s.get_solver().get_model(mdl); - } - if (mdl) { - // serialize access to m_serialize_manager - std::lock_guard lock(m_mutex); - if (!m_serialize_manager) - m_serialize_manager = alloc(ast_manager, s.m(), true); - ast_translation tr(s.m(), *m_serialize_manager); - mdl = mdl->translate(tr); - m_models.push_back(mdl.get()); - } - else if (m_models.empty()) { - if (!m_has_undef) { - m_has_undef = true; - m_reason_undef = "incomplete"; + bb_candidates find_backbone_candidates(expr_ref_vector const& cube, unsigned k = 10) { + bb_candidates result; + vector cands; + try { + s->get_backbone_candidates(cands, s->get_num_bool_vars()); + } catch (z3_error& err) { + if (!m.limit().is_canceled()) + b.set_exception(err.error_code()); + return result; + } catch (z3_exception &ex) { + if (!m.limit().is_canceled() && !is_cancellation_exception(ex.what())) + b.set_exception(ex.what()); + return result; + } catch (...) { + if (!m.limit().is_canceled()) + b.set_exception("unknown exception"); + return result; } - } - if (!m_allsat) { - m_queue.shutdown(); - } - } - - void inc_unsat(solver_state& s) { - std::lock_guard lock(m_mutex); - ++m_num_unsat; - m_last_depth = s.get_depth(); - } - - void report_unsat(solver_state& s) { - inc_unsat(s); - close_branch(s, l_false); - if (s.has_assumptions()) { - expr_ref_vector core(s.m()); - s.get_solver().get_unsat_core(core); - collect_core(core); - } - } - - void report_undef(solver_state& s, std::string const& reason) { - { - std::lock_guard lock(m_mutex); - if (!m_has_undef) { - m_has_undef = true; - m_reason_undef = reason; + for (auto const& cand : cands) { + expr* lit = cand.lit.get(); + if (m_config.m_global_backbones && + b.is_global_backbone_or_negation(m_l2g, lit)) + continue; + result.push_back(bb_candidate(m, lit, cand.score, 1)); + if (result.size() >= k) + break; } + return result; } - close_branch(s, l_undef); - } - void cube_and_conquer(solver_state& s) { - ast_manager& m = s.m(); - vector cube, hard_cubes, cubes; - expr_ref_vector vars(m); - unsigned num_simplifications = 0; + public: - cube_again: - if (canceled(s)) return; - // extract up to one cube and add it. - cube.reset(); - cube.append(s.split_cubes(1)); - SASSERT(cube.size() <= 1); - IF_VERBOSE(2, verbose_stream() << "(tactic.parallel :split-cube " << cube.size() << ")\n";); - { - // std::lock_guard lock(m_mutex); - if (!s.cubes().empty()) m_queue.add_task(s.clone()); - } - if (!cube.empty()) { - s.assert_cube(cube.get(0).cube()); - vars.reset(); - vars.append(cube.get(0).vars()); - } - num_simplifications = 0; + worker(unsigned id, parallel_solver& p, solver& src, params_ref const& params, expr_ref_vector const& src_asms) + : id(id), b(p.m_batch_manager), asms(m), m_g2l(src.get_manager(), m), m_l2g(m, src.get_manager()) { + parallel_params pp(params); + m_config.m_core_minimize = pp.core_minimize(); + m_config.m_ablate_backtracking = pp.ablate_backtracking(); + m_config.m_global_backbones = pp.num_bb_threads() > 0; + if (m_config.m_ablate_backtracking) + m_config.m_core_minimize = false; - simplify_again: - ++num_simplifications; - // simplify - s.inc_depth(1); - if (canceled(s)) return; - switch (s.simplify()) { - case l_undef: break; - case l_true: report_sat(s, nullptr); return; - case l_false: report_unsat(s); return; + s = src.translate(m, params); + // don't share initial units + s->pop_to_base_level(); + m_shared_units_prefix = s->get_assigned_literals().size(); + m_num_initial_atoms = s->get_num_bool_vars(); + // avoid preprocessing lemmas that are exchanged + s->set_preprocess(false); + + for (expr* a : src_asms) + asms.push_back(m_g2l(a)); + + LOG_WORKER(1, " created with " << asms.size() << " assumptions\n"); } - if (canceled(s)) return; - if (s.giveup()) { report_undef(s, s.get_solver().reason_unknown()); return; } - - if (memory_pressure()) { - goto simplify_again; - } - // extract cubes. - cubes.reset(); - s.set_cube_params(); - solver_ref conquer; - - unsigned cutoff = UINT_MAX; - bool first = true; - unsigned num_backtracks = 0, width = 0; - while (cutoff > 0 && !canceled(s)) { - expr_ref_vector c = s.get_solver().cube(vars, cutoff); - if (c.empty() || (cube.size() == 1 && m.is_true(c.back()))) { - if (num_simplifications > 1) { - report_undef(s, std::string("cube simplifications exceeded")); + + 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)) { + LOG_WORKER(1, " no more cubes\n"); return; } - goto simplify_again; - } - if (m.is_false(c.back())) { - break; - } - lbool is_sat = l_undef; - if (!s.has_assumptions() && width >= m_conquer_delay && !conquer) { - conquer = s.copy_solver(); - s.set_conquer_params(*conquer.get()); - } - if (conquer) { - is_sat = conquer->check_sat(c); - } - DEBUG_CODE(for (expr* e : c) SASSERT(e);); - switch (is_sat) { - case l_false: - cutoff = c.size(); - backtrack(*conquer.get(), c, (num_backtracks++) % m_backtrack_frequency == 0); - if (cutoff != c.size()) { - IF_VERBOSE(0, verbose_stream() << "(tactic.parallel :backtrack " << cutoff << " -> " << c.size() << ")\n"); - cutoff = c.size(); + 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(cube); + b.collect_backbone_candidates(m_l2g, local_candidates); + bool lease_canceled = false; + if (!b.checkpoint_worker(id, lease, lease_canceled)) + return; + if (lease_canceled) { + LOG_WORKER(1, " abandoning canceled lease\n"); + continue; + } } - inc_unsat(s); - log_branches(l_false); - break; + lbool r = check_cube(cube); - case l_true: - report_sat(s, conquer.get()); - if (conquer) { - collect_statistics(*conquer.get()); + bool lease_canceled = false; + if (!b.checkpoint_worker(id, lease, lease_canceled)) + return; + if (lease_canceled) { + LOG_WORKER(1, " abandoning canceled lease\n"); + continue; } - return; - case l_undef: - ++width; - IF_VERBOSE(2, verbose_stream() << "(tactic.parallel :cube " << c.size() << " :vars " << vars.size() << ")\n"); - cubes.push_back(cube_var(c, vars)); - cutoff = UINT_MAX; - break; + switch (r) { - } - if (cubes.size() >= conquer_batch_size()) { - spawn_cubes(s, 10*width, cubes); - first = false; - cubes.reset(); + case l_undef: { + LOG_WORKER(1, " found undef cube\n"); + // Escalating the conflict budget (update_max_thread_conflicts) + // or splitting the cube only helps when the cube was abandoned + // because the per-cube conflict limit was reached. For any other + // source of incompleteness (an incomplete theory, quantifiers, + // lambdas, resource limits, ...) the verdict will not change, so + // re-checking the same cube would spin forever. Record a sound + // 'unknown' verdict and stop working this branch instead. + std::string reason = s->reason_unknown(); + if (reason != "max-conflicts-reached") { + LOG_WORKER(1, " undef cube is not conflict-limited (" << reason << "); reporting unknown\n"); + b.set_unknown(reason); + return; + } + update_max_thread_conflicts(); + if (m_config.m_max_cube_depth <= cube.size()) + goto check_cube_start; + expr_ref atom = get_split_atom(lease, cube); + if (atom) { + b.try_split(m_l2g, id, lease, atom.get(), m_config.m_threads_max_conflicts); + } + else { + goto check_cube_start; + } + break; + } + + case l_true: { + LOG_WORKER(1, " found sat cube\n"); + model_ref mdl; + s->get_model(mdl); + if (mdl) + b.set_sat(m_l2g, *mdl); + return; + } + + case l_false: { + expr_ref_vector unsat_core(m); + s->get_unsat_core(unsat_core); + LOG_WORKER(2, " unsat core:\n"; + for (auto c : unsat_core) verbose_stream() << mk_bounded_pp(c, m, 3) << "\n"); + + if (all_of(unsat_core, [&](expr* e) { return asms.contains(e); })) { + LOG_WORKER(1, " determined formula unsat\n"); + b.set_unsat(m_l2g, unsat_core); + return; + } + + LOG_WORKER(1, " found unsat cube\n"); + auto* source = lease.leased_node; + expr_ref_vector const& core_to_use = m_config.m_ablate_backtracking ? cube : unsat_core; + b.backtrack(m_l2g, id, core_to_use, lease); + + if (m_config.m_core_minimize) + b.enqueue_core_minimization(m_l2g, source, unsat_core); + + if (m_config.m_share_conflicts) + b.collect_clause(m_l2g, id, mk_not(mk_and(unsat_core))); + break; + } + + } + if (m_config.m_share_units) + share_units(); } } - if (conquer) { - collect_statistics(*conquer.get()); + void cancel() { + LOG_WORKER(1, " canceling\n"); + m.limit().cancel(); } - if (cubes.empty() && first) { - report_unsat(s); + void cancel_lease() { + LOG_WORKER(1, " canceling lease\n"); + m.limit().inc_cancel(); } - else if (cubes.empty()) { - dec_branch(); + + void collect_statistics(statistics& st) const { + s->collect_statistics(st); } - else { - s.inc_width(width); - add_branches(cubes.size()-1); - s.set_cubes(cubes); - goto cube_again; - } - } - void spawn_cubes(solver_state& s, unsigned width, vector& cubes) { - if (cubes.empty()) return; - add_branches(cubes.size()); - s.set_cubes(cubes); - solver_state* s1 = nullptr; - { - // std::lock_guard lock(m_mutex); - s1 = s.clone(); - } - s1->inc_width(width); - m_queue.add_task(s1); - } + reslimit& limit() { return m.limit(); } + }; - /* - * \brief backtrack from unsatisfiable core - */ - void backtrack(solver& s, expr_ref_vector& asms, bool full) { - ast_manager& m = s.get_manager(); - expr_ref_vector core(m); - obj_hashtable hcore; - s.get_unsat_core(core); - while (!asms.empty() && !core.contains(asms.back())) asms.pop_back(); - if (!full) return; + class backbones_worker { + private: + 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; + }; - //s.assert_expr(m.mk_not(mk_and(core))); - if (!asms.empty()) { - expr* last = asms.back(); - expr_ref not_last(mk_not(m, last), m); - asms.pop_back(); - asms.push_back(not_last); - lbool r = s.check_sat(asms); - asms.pop_back(); - if (r != l_false) { - asms.push_back(last); - return; + unsigned id; + batch_manager& b; + ast_manager m; + ref s; + expr_ref_vector asms; + ast_translation m_g2l, m_l2g; + unsigned m_bb_chunk_size = 20; + unsigned m_bb_conflicts_per_chunk = 1000; + unsigned m_shared_clause_limit = 0; + stats m_stats; + unsigned m_shared_units_prefix = 0; + unsigned m_num_initial_atoms = 0; + bool m_positive_mode = false; + + void collect_shared_clauses() { + expr_ref_vector nc = b.return_shared_clauses(m_g2l, m_shared_clause_limit, UINT_MAX); + for (expr* e : nc) { + s->assert_expr(e); + LOG_BB_WORKER(4, " asserting shared clause: " << mk_bounded_pp(e, m, 3) << "\n"); } - core.reset(); - s.get_unsat_core(core); - if (core.contains(not_last)) { - //s.assert_expr(m.mk_not(mk_and(core))); - r = s.check_sat(asms); - } - if (r == l_false) { - backtrack(s, asms, full); - } - } - } - - bool canceled(solver_state& s) { - if (s.canceled()) { - m_has_undef = true; - return true; } - else { + + bool try_get_unit_backbone(expr* candidate, expr_ref& backbone) { + expr_ref_vector trail = s->get_assigned_literals(); + expr* atom = candidate; + m.is_not(candidate, atom); + for (expr* e : trail) { + if (s->get_assign_level(e) > 0) + continue; + expr* trail_atom = e; + m.is_not(e, trail_atom); + if (trail_atom != atom) + continue; + backbone = expr_ref(e, m); + return true; + } return false; - } - } + } - bool memory_pressure() { - return memory::above_high_watermark(); - } + lbool probe_literal(expr* e, bool is_retry) { + lbool r = l_undef; + try { + asms.push_back(e); + r = s->check_sat(asms); + asms.pop_back(); + } + catch (z3_error& err) { + asms.pop_back(); + if (!m.limit().is_canceled()) + b.set_exception(err.error_code()); + } + catch (z3_exception& ex) { + asms.pop_back(); + if (!m.limit().is_canceled() && !is_cancellation_exception(ex.what())) + b.set_exception(ex.what()); + } - void run_solver() { - try { - while (solver_state* st = m_queue.get_task()) { - cube_and_conquer(*st); - collect_statistics(*st); - m_queue.task_done(st); - if (!st->m().inc()) m_queue.shutdown(); - IF_VERBOSE(2, display(verbose_stream());); - dealloc(st); + if (r == l_false) { + expr_ref_vector core(m); + s->get_unsat_core(core); + if (!core.contains(e)) { + b.set_unsat(m_l2g, core); + return l_false; + } + expr_ref bb(mk_not(m, e), m); + ++m_stats.m_backbones_detected; + if (b.collect_global_backbone(m_l2g, bb)) { + ++m_stats.m_internal_backbones_found; + if (is_retry) + ++m_stats.m_retry_backbones_found; + } + s->assert_expr(bb.get()); + return l_undef; + } + if (r == l_true) { + model_ref mdl; + s->get_model(mdl); + if (mdl) + b.set_sat(m_l2g, *mdl); + } + return r; + } + + void run_batch_mode() { + bb_candidates curr_batch; + + while (m.inc()) { + if (!b.wait_for_backbone_job(id, m_g2l, curr_batch, m.limit())) { + LOG_BB_WORKER(1, " BACKBONES WORKER cancelling\n"); + return; + } + + if (curr_batch.empty()) + continue; + + LOG_BB_WORKER(1, " received batch of " << curr_batch.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) { + if (is_retry) + ++m_stats.m_bb_retries; + else + ++m_stats.m_fallback_singleton_checks; + + unsigned old_max_conflicts = s->get_max_conflicts(); + s->set_max_conflicts(10); + + for (expr* lit : chunk_lits) { + if (is_retry && b.has_new_backbone_candidates(bb_candidate_epoch)) { + s->set_max_conflicts(old_max_conflicts); + return; + } + if (!m.inc() || canceled()) { + s->set_max_conflicts(old_max_conflicts); + return; + } + if (!bb_candidate_lits.contains(lit)) + continue; + + expr_ref bb_ref(lit, m); + if (m_positive_mode) + bb_ref = mk_not(m, bb_ref); + + if (!b.is_global_backbone_or_negation(m_l2g, bb_ref.get())) { + expr_ref backbone(m); + if (try_get_unit_backbone(bb_ref.get(), backbone)) { + ++m_stats.m_backbones_detected; + if (b.collect_global_backbone(m_l2g, backbone)) { + ++m_stats.m_internal_backbones_found; + if (is_retry) + ++m_stats.m_retry_backbones_found; + } + LOG_BB_WORKER(1, " fallback found unit backbone: " << mk_bounded_pp(backbone.get(), m, 3) << "\n"); + } + else { + expr* atom = bb_ref.get(); + m.is_not(bb_ref.get(), atom); + if (s->get_bool_var(atom) != UINT_MAX) { + lbool terminal_result = probe_literal(mk_not(m, bb_ref), is_retry); + LOG_BB_WORKER(1, " RESULT: " << terminal_result << " FOR CANDIDATE: " << mk_bounded_pp(bb_ref.get(), m, 3) << "\n"); + if (terminal_result != l_undef) { + s->set_max_conflicts(old_max_conflicts); + return; + } + } + } + } + bb_candidate_lits.erase(lit); + } + + s->set_max_conflicts(old_max_conflicts); + }; + + ++m_stats.m_batches_total; + m_stats.m_candidates_total += curr_batch.size(); + + expr_ref_vector bb_candidate_lits(m); + for (auto const& c : curr_batch) + bb_candidate_lits.push_back(c.lit); + + unsigned chunk_delta = 1; + + while (!bb_candidate_lits.empty() && !canceled() && m.inc()) { + { + unsigned j = 0; + for (expr* lit : bb_candidate_lits) { + if (!b.is_global_backbone_or_negation(m_l2g, lit)) + bb_candidate_lits[j++] = lit; + } + bb_candidate_lits.shrink(j); + } + + { + unsigned j = 0; + for (expr* lit : bb_candidate_lits) { + expr_ref backbone(m); + if (try_get_unit_backbone(lit, backbone)) { + 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; + + 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_positive_mode) + bb_asms.append(negated_chunk_lits); + else + bb_asms.append(chunk_lits); + + collect_shared_clauses(); + + while (true) { + if (!m.inc()) { + b.set_canceled(); + 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); + + s->set_max_conflicts(m_bb_conflicts_per_chunk); + lbool r = l_undef; + try { + r = s->check_sat(asms); + } + catch (z3_error& err) { + if (!m.limit().is_canceled()) + b.set_exception(err.error_code()); + } + catch (z3_exception& ex) { + if (!m.limit().is_canceled() && !is_cancellation_exception(ex.what())) + b.set_exception(ex.what()); + } + 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; + ++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; + s->get_model(mdl); + if (mdl) + b.set_sat(m_l2g, *mdl); + curr_batch.reset(); + return; + } + + expr_ref_vector bb_asms_in_core(m); + expr_ref_vector unsat_core(m); + s->get_unsat_core(unsat_core); + + for (expr* a : unsat_core) + if (bb_asms.contains(a)) + bb_asms_in_core.push_back(a); + + if (bb_asms_in_core.empty()) { + b.set_unsat(m_l2g, unsat_core); + return; + } + + 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; + s->assert_expr(backbone_lit.get()); + } + + expr* candidate_to_remove = + (!m_positive_mode) ? backbone_lit.get() : a; + 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; + } + } + } + + while (!b.has_new_backbone_candidates(bb_candidate_epoch) && !canceled() && m.inc()) { + collect_shared_clauses(); + unsigned found_before = m_stats.m_internal_backbones_found; + + 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 : curr_batch) + 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); + + if (m_stats.m_internal_backbones_found == found_before) + break; + } + + if (!canceled()) + b.cancel_current_backbone_batch(); + + curr_batch.reset(); } } - catch (z3_exception& ex) { - IF_VERBOSE(1, verbose_stream() << ex.what() << "\n";); - if (m_queue.in_shutdown()) return; - m_queue.shutdown(); - std::lock_guard lock(m_mutex); - if (ex.has_error_code()) { - m_exn_code = ex.error_code(); - } - else { - m_exn_msg = ex.what(); - m_exn_code = -1; + + public: + backbones_worker(unsigned id, parallel_solver& p, solver& src, params_ref const& params, + expr_ref_vector const& src_asms) + : id(id), b(p.m_batch_manager), + asms(m), m_g2l(src.get_manager(), m), m_l2g(m, src.get_manager()) { + s = src.translate(m, params); + s->set_max_conflicts(m_bb_conflicts_per_chunk); + s->pop_to_base_level(); + for (expr* a : src_asms) + asms.push_back(m_g2l(a)); + m_positive_mode = id != 0; + m_shared_units_prefix = s->get_assigned_literals().size(); + m_num_initial_atoms = s->get_num_bool_vars(); + IF_VERBOSE(1, verbose_stream() << "Initialized backbones thread " << id << "\n"); + } + + void run() { run_batch_mode(); } + + void cancel() { + LOG_BB_WORKER(1, " BACKBONES WORKER cancelling\n"); + m.limit().cancel(); + } + + void collect_statistics(statistics& st) const { + st.update("parallel-bb-batches-total", m_stats.m_batches_total); + st.update("parallel-bb-candidates-total", m_stats.m_candidates_total); + st.update("parallel-bb-backbones-detected", m_stats.m_backbones_detected); + st.update("parallel-bb-internal-backbones-found", m_stats.m_internal_backbones_found); + st.update("parallel-bb-retry-backbones-found", m_stats.m_retry_backbones_found); + st.update("parallel-bb-retries", m_stats.m_bb_retries); + st.update("parallel-bb-core-refinement-rounds", m_stats.m_core_refinement_rounds); + st.update("parallel-bb-singleton-backbones", m_stats.m_singleton_backbones); + st.update("parallel-bb-fallback-singleton-checks", m_stats.m_fallback_singleton_checks); + st.update("parallel-bb-fallback-chunk-exhausted", m_stats.m_fallback_reason_chunk_exhausted); + st.update("parallel-bb-fallback-undef", m_stats.m_fallback_reason_undef); + st.update("parallel-bb-literals-removed-by-core", m_stats.m_lits_removed_by_core); + st.update("parallel-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("parallel-bb-backbone-yield-pct", + 100.0 * safe_ratio(m_stats.m_internal_backbones_found, m_stats.m_candidates_total)); + st.update("parallel-bb-avg-backbones-per-batch", + safe_ratio(m_stats.m_internal_backbones_found, m_stats.m_batches_total)); + st.update("parallel-bb-core-refinement-rounds-per-batch", + safe_ratio(m_stats.m_core_refinement_rounds, m_stats.m_batches_total)); + st.update("parallel-bb-core-effectiveness-lit-removed-per-round", + safe_ratio(m_stats.m_lits_removed_by_core, m_stats.m_core_refinement_rounds)); + } + + reslimit& limit() { return m.limit(); } + }; + + class core_minimizer_worker { + batch_manager& b; + ast_manager m; + ref s; + expr_ref_vector asms; + 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 collect_shared_clauses() { + expr_ref_vector nc = b.return_shared_clauses(m_g2l, m_shared_clause_limit, UINT_MAX); + for (expr* e : nc) { + s->assert_expr(e); + IF_VERBOSE(4, verbose_stream() << "Core minimizer asserting shared clause: " + << mk_bounded_pp(e, m, 3) << "\n";); } } + + void minimize_unsat_core(expr_ref_vector& core) { + expr_ref_vector unknown(core), mus(m), trial(m); + unsigned original_size = core.size(); + ++m_num_core_minimize_calls; + + 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 { + s->set_max_conflicts(m_core_minimize_conflict_budget); + r = s->check_sat(trial); + } + catch (z3_error&) { + r = l_undef; + } + catch (z3_exception&) { + r = l_undef; + } + + switch (r) { + case l_undef: + ++m_num_core_minimize_undef; + mus.push_back(lit); + break; + case l_true: { + if (!asms.empty()) { + mus.push_back(lit); + break; + } + ++m_num_core_minimize_found_sat; + model_ref mdl; + s->get_model(mdl); + if (mdl) + b.set_sat(m_l2g, *mdl); + return; + } + case l_false: { + expr_ref_vector unsat_core(m); + s->get_unsat_core(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); + if (core.size() < original_size) + m_num_core_minimize_lits_removed += original_size - core.size(); + } + + public: + core_minimizer_worker(parallel_solver& p, solver& src, params_ref const& params, + expr_ref_vector const& src_asms) + : b(p.m_batch_manager), + asms(m), m_g2l(src.get_manager(), m), m_l2g(m, src.get_manager()) { + s = src.translate(m, params); + s->pop_to_base_level(); + // avoid preprocessing lemmas that are exchanged + s->set_preprocess(false); + for (expr* a : src_asms) + asms.push_back(m_g2l(a)); + IF_VERBOSE(1, verbose_stream() << "Initialized core minimizer thread\n"); + } + + void run() { + while (m.inc()) { + search_tree::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); + + if (minimized.size() <= 1) + continue; + + minimize_unsat_core(minimized); + + if (minimized.size() < original_size) + b.publish_minimized_core(m_l2g, asms, source, original_size, minimized); + } + b.set_canceled(); + } + + void cancel() { + IF_VERBOSE(1, verbose_stream() << "Core minimizer cancelling\n"); + m.limit().cancel(); + } + + void collect_statistics(statistics& st) const { + 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); + } + + reslimit& limit() { return m.limit(); } + }; + + /* ---- members ---- */ + ref m_solver; + ast_manager& m_manager; + params_ref m_params; + scoped_ptr_vector m_workers; + scoped_ptr m_core_minimizer_worker; + scoped_ptr_vector m_global_backbones_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) {} + + params_ref mk_worker_params(unsigned seed_offset) const { + params_ref p(m_params); + // Match smt_parallel's per-worker m_smt_params.m_random_seed += id. + // Generic solver workers receive the seed through translate params. + unsigned base_seed = m_params.get_uint("random_seed", 0); + p.set_uint("random_seed", base_seed + seed_offset); + p.set_uint("threads", 1); + return p; } - void collect_statistics(solver_state& s) { - collect_statistics(s.get_solver()); - } + /* 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) { - void collect_statistics(solver& s) { - std::lock_guard lock(m_mutex); - s.collect_statistics(m_stats); - } + parallel_params pp(m_params); + unsigned num_threads = std::min( + static_cast(std::thread::hardware_concurrency()), + pp.threads_max()); + bool core_minimize = pp.core_minimize(); + unsigned num_bb_threads = pp.num_bb_threads(); + if (num_bb_threads > 2) + throw default_exception("parallel.num_bb_threads must be 0, 1, or 2"); + if (num_threads > num_bb_threads + 2) + num_threads -= num_bb_threads; + else + num_bb_threads = 0; + if (core_minimize && num_threads > 2) + num_threads -= 1; + else + core_minimize = false; + unsigned total_threads = num_threads + (core_minimize ? 1 : 0) + num_bb_threads; - lbool solve(model_ref& mdl) { - add_branches(1); + IF_VERBOSE(1, verbose_stream() << "Parallel tactical2 with " << total_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()); + + // Set up the source before translating workers. SMT context copies + // then run initial internalization/preprocessing before workers disable + // preprocessing for exchanged lemmas. + m_solver->setup_for_parallel(); + + for (unsigned i = 0; i < num_threads; ++i) { + params_ref worker_params = mk_worker_params(i); + auto* w = alloc(worker, i, *this, *m_solver, worker_params, asms); + m_workers.push_back(w); + sl.push_child(&(w->limit())); + } + + m_core_minimizer_worker = nullptr; + if (core_minimize) { + params_ref core_min_params = mk_worker_params(num_threads); + m_core_minimizer_worker = alloc(core_minimizer_worker, *this, *m_solver, core_min_params, asms); + sl.push_child(&(m_core_minimizer_worker->limit())); + } + m_global_backbones_workers.reset(); + for (unsigned i = 0; i < num_bb_threads; ++i) { + params_ref bb_params = mk_worker_params(num_threads + (core_minimize ? 1 : 0) + i); + auto* w = alloc(backbones_worker, i, *this, *m_solver, bb_params, asms); + m_global_backbones_workers.push_back(w); + sl.push_child(&(w->limit())); + } + IF_VERBOSE(1, verbose_stream() << "Launched " << m_workers.size() << " CDCL threads, " + << 0 << " SLS threads, " + << (m_core_minimizer_worker ? 1 : 0) << " core minimizer threads, " + << m_global_backbones_workers.size() << " global backbone threads.\n";); + + m_batch_manager.initialize(num_bb_threads); + + auto safe_run = [&](auto&& run_fn, reslimit& lim) { + try { + run_fn(); + if (lim.is_canceled()) + m_batch_manager.set_canceled(); + } catch (z3_error &err) { + IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << err.what() << "\n"); + if (!lim.is_canceled()) + m_batch_manager.set_exception(err.error_code()); + else + m_batch_manager.set_canceled(); + } catch (z3_exception &ex) { + IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << ex.what() << "\n"); + if (!lim.is_canceled() && !is_cancellation_exception(ex.what())) + m_batch_manager.set_exception(ex.what()); + else + m_batch_manager.set_canceled(); + } catch (...) { + IF_VERBOSE(0, verbose_stream() << "Unknown exception in parallel solver\n"); + if (!lim.is_canceled()) + m_batch_manager.set_exception("unknown exception"); + else + m_batch_manager.set_canceled(); + } + }; + + /* Launch threads. */ vector threads; - for (unsigned i = 0; i < m_num_threads; ++i) - threads.push_back(std::thread([this]() { run_solver(); })); - for (std::thread& t : threads) + for (auto *w : m_workers) + threads.push_back(std::thread([w, &safe_run]() { + safe_run([w]() { w->run(); }, w->limit()); + })); + if (m_core_minimizer_worker) + threads.push_back(std::thread([this, &safe_run]() { + safe_run([this]() { m_core_minimizer_worker->run(); }, m_core_minimizer_worker->limit()); + })); + for (auto* w : m_global_backbones_workers) + threads.push_back(std::thread([w, &safe_run]() { + safe_run([w]() { w->run(); }, w->limit()); + })); + + for (auto& t : threads) t.join(); - m_queue.stats(m_stats); + + m_solver->reset_statistics(); + statistics aux; + for (auto* w : m_workers) { + aux.reset(); + w->collect_statistics(aux); + m_solver->add_statistics(aux); + } + aux.reset(); + m_batch_manager.collect_statistics(aux); + m_solver->add_statistics(aux); + if (m_core_minimizer_worker) { + aux.reset(); + m_core_minimizer_worker->collect_statistics(aux); + m_solver->add_statistics(aux); + } + for (auto* w : m_global_backbones_workers) { + aux.reset(); + w->collect_statistics(aux); + m_solver->add_statistics(aux); + } + m_stats.reset(); + m_solver->collect_statistics(m_stats); + m_manager.limit().reset_cancel(); - if (m_exn_code == -1) - throw default_exception(std::move(m_exn_msg)); - if (m_exn_code != 0) - throw z3_error(m_exn_code); - // retrieve model. The ast manager of the model is m_serialize_manager. - // the asts have to be translated into m_manager. - if (!m_models.empty()) { - mdl = m_models.back(); - ast_translation tr(mdl->get_manager(), m_manager); - mdl = mdl->translate(tr); - return l_true; + 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); } - if (m_has_undef) - return l_undef; - return l_false; + + sl.reset(); + m_workers.reset(); + m_core_minimizer_worker = nullptr; + m_global_backbones_workers.reset(); + return result; } - std::ostream& display(std::ostream& out) { - unsigned n_models, n_unsat; - double n_progress; - statistics st; - { - std::lock_guard lock(m_mutex); - n_models = m_models.size(); - n_unsat = m_num_unsat; - n_progress = m_progress; - st.copy(m_stats); - } - st.display(out); - m_queue.display(out); - out << "(tactic.parallel :unsat " << n_unsat << " :progress " << n_progress << "% :models " << n_models << ")\n"; - return out; + void collect_statistics(statistics& st) const { + st.copy(m_stats); } + std::string reason_unknown() const { + return m_batch_manager.get_reason_unknown(); + } + + void reset_statistics() { + m_stats.reset(); + } +}; + +/* ------------------------------------------------------------------ */ +/* parallel_tactic – wraps parallel_solver as a tactic */ +/* ------------------------------------------------------------------ */ + +class parallel_tactic : public tactic { + + solver_ref m_solver; + ast_manager& m_manager; + params_ref m_params; + statistics m_stats; + public: - parallel_tactic(solver* s, params_ref const& p) : - m_solver(s), - m_manager(s->get_manager()), - m_params(p) { - init(); - } + parallel_tactic(solver* s, params_ref const& p) + : m_solver(s), m_manager(s->get_manager()), m_params(p) {} char const* name() const override { return "parallel_tactic"; } - void operator()(const goal_ref & g,goal_ref_buffer & result) override { - cleanup(); - fail_if_proof_generation("parallel-tactic", g); - ast_manager& m = g->m(); + void operator()(const goal_ref& g, goal_ref_buffer& result) override { + fail_if_proof_generation("parallel_tactic", g); + ast_manager& m = g->m(); + if (m.has_trace_stream()) - throw default_exception("parallel tactic does not work with trace"); + throw default_exception( + "parallel_tactic does not work with trace streams"); + + /* Translate goal into a set of clauses + assumptions. */ solver* s = m_solver->translate(m, m_params); - solver_state* st = alloc(solver_state, nullptr, s, m_params); - m_queue.add_task(st); expr_ref_vector clauses(m); - ptr_vector assumptions; + ptr_vector assumptions_raw; obj_map bool2dep; ref fmc; - expr_dependency * lcore = nullptr; - proof* pr = nullptr; - extract_clauses_and_dependencies(g, clauses, assumptions, bool2dep, fmc); - for (expr * clause : clauses) { - s->assert_expr(clause); - } - st->set_assumptions(assumptions); + 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; - lbool is_sat = solve(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: + case l_true: { + if (g->models_enabled() && mdl) { + model_converter_ref mc = model2model_converter(mdl.get()); + mc = concat(fmc.get(), mc.get()); + mc = concat(s->mc0(), mc.get()); + g->add(mc.get()); + } g->reset(); - if (g->models_enabled()) { - g->add(concat(fmc.get(), model2model_converter(mdl.get()))); - } - break; - case l_false: - SASSERT(!g->proofs_enabled()); - if (m_core) { - ast_translation tr(m_core->get_manager(), m); - expr_ref_vector core(tr(*m_core)); - for (expr * c : core) - lcore = m.mk_join(lcore, m.mk_leaf(bool2dep.find(c))); - } - g->assert_expr(m.mk_false(), pr, lcore); - break; - case l_undef: - if (!m.inc()) - throw tactic_exception(Z3_CANCELED_MSG); - if (m_has_undef) - throw tactic_exception(m_reason_undef.c_str()); 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); + { + std::string reason = ps.reason_unknown(); + if (!reason.empty()) { + g->set_reason_unknown(reason); + IF_VERBOSE(0, verbose_stream() << reason << "\n"); + } + } + break; + } + result.push_back(g.get()); } - unsigned conquer_batch_size() const { - parallel_params pp(m_params); - return pp.conquer_batch_size(); - } - - void cleanup() override { - m_queue.reset(); - m_models.reset(); - } + void cleanup() override {} tactic* translate(ast_manager& m) override { solver* s = m_solver->translate(m, m_params); return alloc(parallel_tactic, s, m_params); } - void updt_params(params_ref const & p) override { + void updt_params(params_ref const& p) override { m_params.copy(p); - parallel_params pp(p); - m_conquer_delay = pp.conquer_delay(); } - void collect_statistics(statistics & st) const override { + void collect_statistics(statistics& st) const override { st.copy(m_stats); - st.update("par unsat", m_num_unsat); - st.update("par models", m_models.size()); - st.update("par progress", m_progress); } void reset_statistics() override { m_stats.reset(); } - }; - -tactic * mk_parallel_tactic(solver* s, params_ref const& p) { +tactic* mk_parallel_tactic(solver* s, params_ref const& p) { return alloc(parallel_tactic, s, p); } diff --git a/src/solver/parallel_tactical.h b/src/solver/parallel_tactical.h index 5d21ad18d4..a09f1cf5d3 100644 --- a/src/solver/parallel_tactical.h +++ b/src/solver/parallel_tactical.h @@ -1,23 +1,25 @@ /*++ -Copyright (c) 2017 Microsoft Corporation +Copyright (c) 2024 Microsoft Corporation Module Name: - parallel_tactic.h + parallel_tactical.h Abstract: - Parallel tactic in the style of Treengeling. + 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: - Nikolaj Bjorner (nbjorner) 2017-10-9 - + (based on smt_parallel.cpp and parallel_tactical.cpp) + --*/ #pragma once class tactic; class solver; +class params_ref; tactic * mk_parallel_tactic(solver* s, params_ref const& p); - diff --git a/src/solver/parallel_tactical2.cpp b/src/solver/parallel_tactical2.cpp deleted file mode 100644 index ebae4cb577..0000000000 --- a/src/solver/parallel_tactical2.cpp +++ /dev/null @@ -1,904 +0,0 @@ -/*++ -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 deleted file mode 100644 index b27a4c7407..0000000000 --- a/src/solver/parallel_tactical2.h +++ /dev/null @@ -1,25 +0,0 @@ -/*++ -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/solver.h b/src/solver/solver.h index b45f4f3477..556b8b27cb 100644 --- a/src/solver/solver.h +++ b/src/solver/solver.h @@ -22,6 +22,7 @@ Notes: #include "solver/check_sat_result.h" #include "solver/progress_callback.h" #include "util/params.h" +#include "util/sat_literal.h" class solver; class model_converter; @@ -58,6 +59,13 @@ class solver : public check_sat_result, public user_propagator::core { params_ref m_params; symbol m_cancel_backup_file; public: + struct scored_literal { + expr_ref lit; + double score = 0.0; + scored_literal(ast_manager& m, expr* e, double s): lit(e, m), score(s) {} + scored_literal(expr_ref const& e, double s): lit(e), score(s) {} + }; + solver(ast_manager& m): check_sat_result(m) {} /** @@ -247,7 +255,9 @@ public: \brief extract a lookahead candidates for branching. */ - virtual expr_ref_vector cube(expr_ref_vector& vars, unsigned backtrack_level) = 0; + virtual expr_ref_vector cube(expr_ref_vector& vars, unsigned backtrack_level=0) = 0; + + virtual expr_ref cube_vsids(expr_ref_vector const&) { return expr_ref(m); } /** \brief retrieve congruence closure root. @@ -298,9 +308,34 @@ public: expr_ref_vector get_non_units(); virtual expr_ref_vector get_trail(unsigned max_level) = 0; // { return expr_ref_vector(get_manager()); } + virtual expr_ref_vector get_assigned_literals() { return get_trail(UINT_MAX); } + virtual unsigned get_assign_level(expr* e) const { return UINT_MAX; } + virtual bool is_relevant(expr* e) const { return true; } + virtual unsigned get_num_bool_vars() const { return UINT_MAX; } + virtual sat::bool_var get_bool_var(expr* e) const { return sat::null_bool_var; } + virtual expr* bool_var2expr(sat::bool_var) const { return nullptr; } + virtual lbool get_assignment(sat::bool_var) const { return l_undef; } + virtual double get_activity(sat::bool_var) const { return 0.0; } + virtual bool was_eliminated(sat::bool_var) const { return false; } + + virtual void pop_to_base_level() {} + + virtual void setup_for_parallel() {} + + virtual void set_preprocess(bool) {} + + virtual void set_max_conflicts(unsigned max_conflicts) { + params_ref p; + p.set_uint("max_conflicts", max_conflicts); + updt_params(p); + } + + virtual unsigned get_max_conflicts() const { return UINT_MAX; } virtual void get_levels(ptr_vector const& vars, unsigned_vector& depth) = 0; + virtual void get_backbone_candidates(vector&, unsigned) {} + class scoped_push { solver& s; bool m_nopop; @@ -328,4 +363,3 @@ typedef ref solver_ref; inline std::ostream& operator<<(std::ostream& out, solver const& s) { return s.display(out); } - diff --git a/src/tactic/arith/lia2card_tactic.cpp b/src/tactic/arith/lia2card_tactic.cpp index 17d8de9080..b454573494 100644 --- a/src/tactic/arith/lia2card_tactic.cpp +++ b/src/tactic/arith/lia2card_tactic.cpp @@ -20,6 +20,7 @@ Notes: #include "ast/ast_ll_pp.h" #include "ast/pb_decl_plugin.h" #include "ast/arith_decl_plugin.h" +#include "ast/recfun_decl_plugin.h" #include "ast/rewriter/rewriter_def.h" #include "ast/rewriter/expr_safe_replace.h" #include "ast/ast_util.h" @@ -185,6 +186,10 @@ public: expr_safe_replace rep(m); tactic_report report("lia2card", *g); + if (recfun::util(m).has_rec_defs()) { + result.push_back(g.get()); + return; + } bound_manager bounds(m); for (unsigned i = 0; i < g->size(); ++i) diff --git a/src/tactic/core/elim_uncnstr_tactic.cpp b/src/tactic/core/elim_uncnstr_tactic.cpp index 9caf76c4a6..6807cdaa1e 100644 --- a/src/tactic/core/elim_uncnstr_tactic.cpp +++ b/src/tactic/core/elim_uncnstr_tactic.cpp @@ -19,6 +19,7 @@ Notes: #include "tactic/tactical.h" #include "ast/converters/generic_model_converter.h" #include "ast/rewriter/rewriter_def.h" +#include "ast/rewriter/seq_rewriter.h" #include "ast/arith_decl_plugin.h" #include "ast/bv_decl_plugin.h" #include "ast/recfun_decl_plugin.h" @@ -803,10 +804,10 @@ class elim_uncnstr_tactic : public tactic { // x ++ y -> z, x -> z, y -> eps app * process_seq_app(func_decl * f, unsigned num, expr * const * args) { + app *r = nullptr; switch (f->get_decl_kind()) { case _OP_STRING_CONCAT: case OP_SEQ_CONCAT: { - app * r = nullptr; expr* x, *y; if (uncnstr(args[0]) && num == 2 && args[1]->get_ref_count() == 1 && @@ -833,6 +834,27 @@ class elim_uncnstr_tactic : public tactic { return r; } + case OP_SEQ_IN_RE: + return nullptr; + if (uncnstr(args[0]) && m_seq_util.re.is_ground(args[1]) && m_seq_util.is_string(args[0]->get_sort())) { + zstring s1; + expr *re = args[1]; + seq_rewriter rw(m()); + if (l_true != rw.some_string_in_re(re, s1)) + return nullptr; + zstring s2; + expr_ref not_re(m_seq_util.re.mk_complement(re), m()); + if (l_true != rw.some_string_in_re(not_re, s2)) + return nullptr; + + mk_fresh_uncnstr_var_for(f, num, args, r); + expr_ref witness1 = expr_ref(m_seq_util.str.mk_string(s1), m()); + expr_ref witness2 = expr_ref(m_seq_util.str.mk_string(s2), m()); + if (m_mc) + add_def(args[0], m().mk_ite(r, witness1, witness2)); + return r; + } + return nullptr; default: return nullptr; } diff --git a/src/tactic/fd_solver/bounded_int2bv_solver.cpp b/src/tactic/fd_solver/bounded_int2bv_solver.cpp index 28cb5823a1..26b57032bc 100644 --- a/src/tactic/fd_solver/bounded_int2bv_solver.cpp +++ b/src/tactic/fd_solver/bounded_int2bv_solver.cpp @@ -179,6 +179,21 @@ public: return m_solver->get_trail(max_level); } + void setup_for_parallel() override { m_solver->setup_for_parallel(); } + void set_max_conflicts(unsigned c) override { m_solver->set_max_conflicts(c); } + unsigned get_max_conflicts() const override { return m_solver->get_max_conflicts(); } + expr_ref_vector get_assigned_literals() override { return m_solver->get_assigned_literals(); } + unsigned get_assign_level(expr* e) const override { flush_assertions(); return m_solver->get_assign_level(e); } + bool is_relevant(expr* e) const override { flush_assertions(); return m_solver->is_relevant(e); } + unsigned get_num_bool_vars() const override { flush_assertions(); return m_solver->get_num_bool_vars(); } + sat::bool_var get_bool_var(expr* e) const override { flush_assertions(); return m_solver->get_bool_var(e); } + expr* bool_var2expr(sat::bool_var v) const override { return m_solver->bool_var2expr(v); } + lbool get_assignment(sat::bool_var v) const override { return m_solver->get_assignment(v); } + double get_activity(sat::bool_var v) const override { return m_solver->get_activity(v); } + bool was_eliminated(sat::bool_var v) const override { return m_solver->was_eliminated(v); } + expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { flush_assertions(); return m_solver->cube_vsids(invalid_split_atoms); } + void get_backbone_candidates(vector& candidates, unsigned max_num) override { flush_assertions(); m_solver->get_backbone_candidates(candidates, max_num); } + model_converter* external_model_converter() const { return concat(mc0(), local_model_converter()); } diff --git a/src/tactic/fd_solver/enum2bv_solver.cpp b/src/tactic/fd_solver/enum2bv_solver.cpp index d420aa7962..55cdddd53a 100644 --- a/src/tactic/fd_solver/enum2bv_solver.cpp +++ b/src/tactic/fd_solver/enum2bv_solver.cpp @@ -195,6 +195,21 @@ public: return m_solver->get_trail(max_level); } + void setup_for_parallel() override { m_solver->setup_for_parallel(); } + void set_max_conflicts(unsigned c) override { m_solver->set_max_conflicts(c); } + unsigned get_max_conflicts() const override { return m_solver->get_max_conflicts(); } + expr_ref_vector get_assigned_literals() override { return m_solver->get_assigned_literals(); } + unsigned get_assign_level(expr* e) const override { return m_solver->get_assign_level(e); } + bool is_relevant(expr* e) const override { return m_solver->is_relevant(e); } + unsigned get_num_bool_vars() const override { return m_solver->get_num_bool_vars(); } + sat::bool_var get_bool_var(expr* e) const override { return m_solver->get_bool_var(e); } + expr* bool_var2expr(sat::bool_var v) const override { return m_solver->bool_var2expr(v); } + lbool get_assignment(sat::bool_var v) const override { return m_solver->get_assignment(v); } + double get_activity(sat::bool_var v) const override { return m_solver->get_activity(v); } + bool was_eliminated(sat::bool_var v) const override { return m_solver->was_eliminated(v); } + expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { return m_solver->cube_vsids(invalid_split_atoms); } + void get_backbone_candidates(vector& candidates, unsigned max_num) override { m_solver->get_backbone_candidates(candidates, max_num); } + unsigned get_num_assertions() const override { return m_solver->get_num_assertions(); } diff --git a/src/tactic/fd_solver/pb2bv_solver.cpp b/src/tactic/fd_solver/pb2bv_solver.cpp index 12ac45042f..8b7803d295 100644 --- a/src/tactic/fd_solver/pb2bv_solver.cpp +++ b/src/tactic/fd_solver/pb2bv_solver.cpp @@ -107,6 +107,21 @@ public: return m_solver->get_trail(max_level); } + void setup_for_parallel() override { m_solver->setup_for_parallel(); } + void set_max_conflicts(unsigned c) override { m_solver->set_max_conflicts(c); } + unsigned get_max_conflicts() const override { return m_solver->get_max_conflicts(); } + expr_ref_vector get_assigned_literals() override { return m_solver->get_assigned_literals(); } + unsigned get_assign_level(expr* e) const override { flush_assertions(); return m_solver->get_assign_level(e); } + bool is_relevant(expr* e) const override { flush_assertions(); return m_solver->is_relevant(e); } + unsigned get_num_bool_vars() const override { flush_assertions(); return m_solver->get_num_bool_vars(); } + sat::bool_var get_bool_var(expr* e) const override { flush_assertions(); return m_solver->get_bool_var(e); } + expr* bool_var2expr(sat::bool_var v) const override { return m_solver->bool_var2expr(v); } + lbool get_assignment(sat::bool_var v) const override { return m_solver->get_assignment(v); } + double get_activity(sat::bool_var v) const override { return m_solver->get_activity(v); } + bool was_eliminated(sat::bool_var v) const override { return m_solver->was_eliminated(v); } + expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { flush_assertions(); return m_solver->cube_vsids(invalid_split_atoms); } + void get_backbone_candidates(vector& candidates, unsigned max_num) override { flush_assertions(); m_solver->get_backbone_candidates(candidates, max_num); } + model_converter* external_model_converter() const{ return concat(mc0(), local_model_converter()); } diff --git a/src/tactic/portfolio/smt_strategic_solver.cpp b/src/tactic/portfolio/smt_strategic_solver.cpp index ffd6450b3a..c60a924754 100644 --- a/src/tactic/portfolio/smt_strategic_solver.cpp +++ b/src/tactic/portfolio/smt_strategic_solver.cpp @@ -43,8 +43,6 @@ Notes: #include "sat/sat_solver/inc_sat_solver.h" #include "sat/sat_solver/sat_smt_solver.h" #include "ast/rewriter/bv_rewriter.h" -#include "solver/solver2tactic.h" -#include "solver/parallel_tactical.h" #include "solver/parallel_params.hpp" #include "params/tactic_params.hpp" #include "parsers/smt2/smt2parser.h" diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index c2519548c3..4563752812 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -3,7 +3,7 @@ add_subdirectory(lp) ################################################################################ # z3-test executable ################################################################################ -set(z3_test_deps api fuzzing simplex smt_seq) +set(z3_test_deps api fuzzing simplex) z3_expand_dependencies(z3_test_expanded_deps ${z3_test_deps}) set (z3_test_extra_object_files "") foreach (component ${z3_test_expanded_deps}) @@ -24,6 +24,7 @@ add_executable(test-z3 api_datalog.cpp parametric_datatype.cpp arith_rewriter.cpp + seq_rewriter.cpp arith_simplifier_plugin.cpp ast.cpp bdd.cpp @@ -52,8 +53,6 @@ add_executable(test-z3 escaped.cpp euf_bv_plugin.cpp euf_arith_plugin.cpp - euf_sgraph.cpp - euf_seq_plugin.cpp ex.cpp expr_rand.cpp expr_substitution.cpp @@ -81,6 +80,7 @@ add_executable(test-z3 "${CMAKE_CURRENT_BINARY_DIR}/install_tactic.cpp" interval.cpp karr.cpp + lcube.cpp list.cpp main.cpp map.cpp @@ -115,15 +115,19 @@ add_executable(test-z3 polynomial_factorization.cpp polynorm.cpp prime_generator.cpp + psmt.cpp + seq_regex_bisim.cpp proof_checker.cpp qe_arith.cpp mbp_qel.cpp quant_elim.cpp quant_solve.cpp random.cpp + range_predicate.cpp rational.cpp rcf.cpp region.cpp + regex_range_collapse.cpp sat_local_search.cpp sat_lookahead.cpp sat_user_scope.cpp @@ -135,11 +139,6 @@ add_executable(test-z3 sls_test.cpp sls_seq_plugin.cpp seq_split.cpp - seq_nielsen.cpp - seq_parikh.cpp - nseq_basic.cpp - seq_regex.cpp - nseq_zipt.cpp small_object_allocator.cpp smt2print_parse.cpp smt_context.cpp @@ -151,6 +150,7 @@ add_executable(test-z3 symbol.cpp symbol_table.cpp tbv.cpp + term_enumeration.cpp theory_dl.cpp theory_pb.cpp timeout.cpp diff --git a/src/test/ackermannize.cpp b/src/test/ackermannize.cpp index 7dbca06bef..10a415bf80 100644 --- a/src/test/ackermannize.cpp +++ b/src/test/ackermannize.cpp @@ -175,7 +175,7 @@ static void test_ackr_bound_probe() { // by the BV solver back to a model for the original formula (with UF). // The two null-pointer guards in ackr_model_converter.cpp are exercised here. // -static void test_ackermannize_bv_model() { +[[maybe_unused]] static void test_ackermannize_bv_model() { Z3_config cfg = Z3_mk_config(); Z3_context ctx = Z3_mk_context(cfg); Z3_del_config(cfg); diff --git a/src/test/api.cpp b/src/test/api.cpp index a7e7f329b9..b1765fdd4a 100644 --- a/src/test/api.cpp +++ b/src/test/api.cpp @@ -81,6 +81,29 @@ void test_bvneg() { std::cout << r << "\n"; } + { + Z3_sort bv1 = Z3_mk_bv_sort(ctx, 1); + Z3_sort bv64 = Z3_mk_bv_sort(ctx, 64); + Z3_ast x = Z3_mk_fresh_const(ctx, "x", bv1); + Z3_ast sx = Z3_mk_sign_ext(ctx, 63, x); + Z3_ast zero = Z3_mk_int64(ctx, 0, bv64); + Z3_ast minus_one = Z3_mk_int64(ctx, -1, bv64); + Z3_ast args[2] = { Z3_mk_eq(ctx, sx, zero), Z3_mk_eq(ctx, sx, minus_one) }; + Z3_ast claim = Z3_mk_or(ctx, 2, args); + + Z3_solver_push(ctx, s); + Z3_solver_assert(ctx, s, Z3_mk_not(ctx, claim)); + ENSURE(Z3_solver_check(ctx, s) == Z3_L_FALSE); + Z3_solver_pop(ctx, s, 1); + + Z3_solver_push(ctx, s); + Z3_solver_assert(ctx, s, Z3_mk_eq(ctx, sx, minus_one)); + std::string smt2 = Z3_solver_to_string(ctx, s); + // Bit-vector numerals must print in canonical unsigned SMT2 form. + ENSURE(smt2.find("(_ bv-") == std::string::npos); + Z3_solver_pop(ctx, s, 1); + } + Z3_solver_dec_ref(ctx, s); Z3_del_config(cfg); Z3_del_context(ctx); diff --git a/src/test/deep_api_bugs.cpp b/src/test/deep_api_bugs.cpp index 206daf5e6f..a977b9cc72 100644 --- a/src/test/deep_api_bugs.cpp +++ b/src/test/deep_api_bugs.cpp @@ -297,7 +297,7 @@ static void test_bug_translate_null_target() { // Z3_translate(ctx, x, nullptr) would crash - no null check on target // The function checks c == target (line 1517) but doesn't check target != nullptr first // So mk_c(target) on line 1522 dereferences nullptr - Z3_error_code err = Z3_get_error_code(ctx); + Z3_get_error_code(ctx); std::cout << " [BUG DOCUMENTED] Z3_translate(ctx, ast, nullptr) would crash\n"; std::cout << " No null check on target before mk_c(target) at api_ast.cpp:1522\n"; @@ -596,7 +596,6 @@ static void test_bug_array_sort_mismatch() { // Create Array(Int, Int) Z3_sort int_sort = Z3_mk_int_sort(ctx); - Z3_sort bool_sort = Z3_mk_bool_sort(ctx); Z3_sort arr_sort = Z3_mk_array_sort(ctx, int_sort, int_sort); Z3_ast arr = Z3_mk_const(ctx, Z3_mk_string_symbol(ctx, "a"), arr_sort); @@ -651,7 +650,7 @@ static void test_bug_substitute_null_arrays() { // With num_exprs=0, null arrays should be fine Z3_ast r = Z3_substitute(ctx, x, 0, nullptr, nullptr); - Z3_error_code err = Z3_get_error_code(ctx); + Z3_get_error_code(ctx); if (r != nullptr) { std::cout << " substitute(x, 0, null, null) OK: " << Z3_ast_to_string(ctx, r) << "\n"; } diff --git a/src/test/dlist.cpp b/src/test/dlist.cpp index e378ddec72..3e472b838b 100644 --- a/src/test/dlist.cpp +++ b/src/test/dlist.cpp @@ -70,7 +70,7 @@ static void test_pop() { TestNode* list = nullptr; TestNode node1(1); TestNode::push_to_front(list, &node1); - TestNode* popped = TestNode::pop(list); + [[maybe_unused]] TestNode* popped = TestNode::pop(list); SASSERT(popped == &node1); SASSERT(list == nullptr); SASSERT(popped->next() == popped); diff --git a/src/test/doc.cpp b/src/test/doc.cpp index 9ea741152d..85acd1aebf 100644 --- a/src/test/doc.cpp +++ b/src/test/doc.cpp @@ -303,14 +303,13 @@ class test_doc_cls { bool_vector to_merge(N, false); bit_vector discard_cols; discard_cols.resize(N, false); - unsigned num_bits = 1; union_find_default_ctx union_ctx; subset_ints equalities(union_ctx); unsigned lo = N; equalities.mk_var(); for (unsigned i = 1; i < N; ++i) { to_merge[i] = (m_ran(2) == 0); - if (!to_merge[i]) ++num_bits; else lo = i; + if (to_merge[i]) lo = i; equalities.mk_var(); } if (lo == N) return; diff --git a/src/test/hashtable.cpp b/src/test/hashtable.cpp index 1f0b3fb75f..6874f2db1e 100644 --- a/src/test/hashtable.cpp +++ b/src/test/hashtable.cpp @@ -38,7 +38,6 @@ int vals[N]; static void tst1() { int_set h1; - int size = 0; for (int i = 1; i < N; i ++) { int v = rand() % (N / 2); h1.insert(v); @@ -92,7 +91,7 @@ static void tst2() { ENSURE(contains(h1, elem)); n++; } - ENSURE(n == h1.size()); + ENSURE(n == static_cast(h1.size())); } ENSURE(h1.size() == h2.size()); // std::cout << "size: " << h1.size() << ", capacity: " << h1.capacity() << "\n"; std::cout.flush(); @@ -194,7 +193,7 @@ void test_hashtable_iterators() { ht.insert(3); int count = 0; - for (const auto& elem : ht) { + for ([[maybe_unused]] const auto& elem : ht) { ++count; } VERIFY(count == 3); diff --git a/src/test/hilbert_basis.cpp b/src/test/hilbert_basis.cpp index 4aaca1355e..01cb6166a6 100644 --- a/src/test/hilbert_basis.cpp +++ b/src/test/hilbert_basis.cpp @@ -119,7 +119,7 @@ expr_ref hilbert_basis_validate::mk_validate(hilbert_basis& hb) { bool is_initial; hb.get_basis_solution(i, v, is_initial); - for (unsigned j = 0; xs.size() < v.size(); ++j) { + for (; xs.size() < v.size(); ) { xs.push_back(m.mk_fresh_const("x", a.mk_int())); } diff --git a/src/test/lcube.cpp b/src/test/lcube.cpp new file mode 100644 index 0000000000..06acbf0fdf --- /dev/null +++ b/src/test/lcube.cpp @@ -0,0 +1,261 @@ +/*++ + Copyright (c) 2020 Microsoft Corporation + + Module Name: + + lcube.cpp + + Abstract: + + Tests for the largest cube test of Bromberger and Weidenbach + (Fast Cube Tests for LIA Constraint Solving, IJCAR 2016), + implemented in int_cube::find_largest_cube(). + + This file lives directly under src/test (not src/test/lp) so that the + scripts/mk_make.py build, which only compiles the top-level test + directory, links tst_lcube(). + + Author: + + Lev Nachmanson (levnach) + + --*/ + +#include +#include +#include + +#include "util/debug.h" +#include "util/params.h" +#include "math/lp/int_cube.h" +#include "math/lp/int_solver.h" +#include "math/lp/lar_solver.h" +#include "math/lp/numeric_pair.h" + +namespace lp { + +// tests for the largest cube test of Bromberger and Weidenbach +namespace lcube_test { + + struct ineq { + vector> m_coeffs; + lconstraint_kind m_kind; + mpq m_rs; + }; + + // builds for every inequality a term with the bound and solves + static void setup(lar_solver& solver, const vector& ineqs, svector* term_columns = nullptr) { + unsigned term_ext = 1000; + for (const auto& in : ineqs) { + unsigned t = solver.add_term(in.m_coeffs, term_ext++); + solver.add_var_bound(t, in.m_kind, in.m_rs); + if (term_columns) + term_columns->push_back(t); + } + auto st = solver.solve(); + VERIFY(st == lp_status::OPTIMAL || st == lp_status::FEASIBLE); + } + + static void verify_model(const lar_solver& solver, const vector& ineqs) { + for (const auto& in : ineqs) { + impq v; + for (const auto& p : in.m_coeffs) + v += solver.get_column_value(p.second) * p.first; + switch (in.m_kind) { + case lconstraint_kind::LE: VERIFY(v <= impq(in.m_rs)); break; + case lconstraint_kind::GE: VERIFY(v >= impq(in.m_rs)); break; + default: VERIFY(false); + } + } + } + + static void verify_int_values(const lar_solver& solver, std::initializer_list vars) { + for (unsigned j : vars) + VERIFY(solver.get_column_value(j).is_int()); + } + + // The example of Bromberger and Weidenbach: 3x1 - x2 <= 0, -2x1 - x2 <= -2, -2x1 + x2 <= 1. + // The largest cube is smaller than the unit cube, the rounded center is not a solution, + // no coordinate flip repairs it (the only integer solution lies outside the lattice cell + // of the center): expect undef and an intact solver state. + static void test_paper_example_undef() { + std::cout << "lcube: paper example, expecting undef\n"; + lar_solver solver; + unsigned x1 = solver.add_named_var(0, true, "x1"); + unsigned x2 = solver.add_named_var(1, true, "x2"); + vector ineqs; + ineqs.push_back(ineq{{{mpq(3), x1}, {mpq(-1), x2}}, lconstraint_kind::LE, mpq(0)}); + ineqs.push_back(ineq{{{mpq(-2), x1}, {mpq(-1), x2}}, lconstraint_kind::LE, mpq(-2)}); + ineqs.push_back(ineq{{{mpq(-2), x1}, {mpq(1), x2}}, lconstraint_kind::LE, mpq(1)}); + setup(solver, ineqs); + int_solver i_s(solver); + solver.set_int_solver(&i_s); + lia_move m = int_cube(i_s).find_largest_cube(); + std::cout << "lcube returned " << lia_move_to_string(m) << "\n"; + VERIFY(m == lia_move::undef); + VERIFY(solver.ax_is_correct()); + } + + // 3x1 - x2 <= 0, -2x1 - x2 <= -1, -2x1 + x2 <= 1: the largest cube has + // edge 4/17 with the center (3/17, 1) that rounds to the solution (0, 1), + // while the unit cube test fails: the largest cube test is stronger here. + static void test_beats_unit_cube() { + std::cout << "lcube: beating the unit cube test\n"; + lar_solver solver; + unsigned x1 = solver.add_named_var(0, true, "x1"); + unsigned x2 = solver.add_named_var(1, true, "x2"); + vector ineqs; + ineqs.push_back(ineq{{{mpq(3), x1}, {mpq(-1), x2}}, lconstraint_kind::LE, mpq(0)}); + ineqs.push_back(ineq{{{mpq(-2), x1}, {mpq(-1), x2}}, lconstraint_kind::LE, mpq(-1)}); + ineqs.push_back(ineq{{{mpq(-2), x1}, {mpq(1), x2}}, lconstraint_kind::LE, mpq(1)}); + svector tcols; + setup(solver, ineqs, &tcols); + // move the solution to a fractional feasible point: the cube tests only + // run when the current solution has fractional integer variables + solver.set_column_value_test(x1, impq(mpq(1, 4))); + solver.set_column_value_test(x2, impq(mpq(5, 4))); + solver.set_column_value_test(tcols[0], impq(mpq(-1, 2))); + solver.set_column_value_test(tcols[1], impq(mpq(-7, 4))); + solver.set_column_value_test(tcols[2], impq(mpq(3, 4))); + int_solver i_s(solver); + solver.set_int_solver(&i_s); + lia_move m = int_cube(i_s)(); + std::cout << "unit cube returned " << lia_move_to_string(m) << "\n"; + VERIFY(m == lia_move::undef); + m = int_cube(i_s).find_largest_cube(); + std::cout << "lcube returned " << lia_move_to_string(m) << "\n"; + VERIFY(m == lia_move::sat); + verify_int_values(solver, {x1, x2}); + verify_model(solver, ineqs); + } + + // 9/10 <= x + y + r <= 11/10, -11/10 <= x - y + r <= 1/10, 0 <= r <= 1/10, + // x, y integer, r real. The real variable keeps the terms and their bounds + // non-integer. A fractional center, e.g. (1/2, 1/2), rounds to an infeasible + // point that is repaired by flipping one coordinate: expect sat. + static void test_flip_repair() { + std::cout << "lcube: rounding repair\n"; + lar_solver solver; + unsigned x = solver.add_named_var(0, true, "x"); + unsigned y = solver.add_named_var(1, true, "y"); + unsigned r = solver.add_named_var(2, false, "r"); + solver.add_var_bound(r, lconstraint_kind::GE, mpq(0)); + solver.add_var_bound(r, lconstraint_kind::LE, mpq(1, 10)); + vector ineqs; + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(1), y}, {mpq(1), r}}, lconstraint_kind::GE, mpq(9, 10)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(1), y}, {mpq(1), r}}, lconstraint_kind::LE, mpq(11, 10)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(-1), y}, {mpq(1), r}}, lconstraint_kind::GE, mpq(-11, 10)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(-1), y}, {mpq(1), r}}, lconstraint_kind::LE, mpq(1, 10)}); + setup(solver, ineqs); + int_solver i_s(solver); + solver.set_int_solver(&i_s); + lia_move m = int_cube(i_s).find_largest_cube(); + std::cout << "lcube returned " << lia_move_to_string(m) + << ", flip successes: " << solver.settings().stats().m_lcube_flip_success << "\n"; + VERIFY(m == lia_move::sat); + verify_int_values(solver, {x, y}); + verify_model(solver, ineqs); + } + + // 3x + 5y >= 7 alone has infinite lattice width: the edge length is + // unbounded and any cube center of edge >= 1 rounds to a solution. + static void test_infinite_lattice_width() { + std::cout << "lcube: infinite lattice width\n"; + lar_solver solver; + unsigned x = solver.add_named_var(0, true, "x"); + unsigned y = solver.add_named_var(1, true, "y"); + vector ineqs; + ineqs.push_back(ineq{{{mpq(3), x}, {mpq(5), y}}, lconstraint_kind::GE, mpq(7)}); + setup(solver, ineqs); + int_solver i_s(solver); + solver.set_int_solver(&i_s); + lia_move m = int_cube(i_s).find_largest_cube(); + std::cout << "lcube returned " << lia_move_to_string(m) << "\n"; + VERIFY(m == lia_move::sat); + verify_int_values(solver, {x, y}); + verify_model(solver, ineqs); + } + + // 0 <= x + 2y + r <= 8, 0 <= x - 2y + r <= 8: the maximal edge length is + // 8/3 >= 1, so the rounded center is guaranteed to be a solution. + static void test_edge_at_least_one() { + std::cout << "lcube: edge length at least 1\n"; + lar_solver solver; + unsigned x = solver.add_named_var(0, true, "x"); + unsigned y = solver.add_named_var(1, true, "y"); + unsigned r = solver.add_named_var(2, false, "r"); + solver.add_var_bound(r, lconstraint_kind::GE, mpq(0)); + solver.add_var_bound(r, lconstraint_kind::LE, mpq(1, 10)); + vector ineqs; + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(2), y}, {mpq(1), r}}, lconstraint_kind::GE, mpq(0)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(2), y}, {mpq(1), r}}, lconstraint_kind::LE, mpq(8)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(-2), y}, {mpq(1), r}}, lconstraint_kind::GE, mpq(0)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(-2), y}, {mpq(1), r}}, lconstraint_kind::LE, mpq(8)}); + setup(solver, ineqs); + int_solver i_s(solver); + solver.set_int_solver(&i_s); + lia_move m = int_cube(i_s).find_largest_cube(); + std::cout << "lcube returned " << lia_move_to_string(m) << "\n"; + VERIFY(m == lia_move::sat); + verify_int_values(solver, {x, y}); + verify_model(solver, ineqs); + } + + // runs the flip-repair instance through int_solver::check() with the + // lp.lcube parameter set and the cube period lowered to 1: verifies the + // dispatch and the parameter plumbing + static void test_dispatch() { + std::cout << "lcube: dispatch through int_solver::check\n"; + lar_solver solver; + params_ref p; + p.set_bool("lcube", true); + solver.settings().updt_params(p); + VERIFY(solver.settings().lcube()); + solver.settings().m_int_find_cube_period = 1; + unsigned x = solver.add_named_var(0, true, "x"); + unsigned y = solver.add_named_var(1, true, "y"); + unsigned r = solver.add_named_var(2, false, "r"); + solver.add_var_bound(r, lconstraint_kind::GE, mpq(0)); + solver.add_var_bound(r, lconstraint_kind::LE, mpq(1, 10)); + vector ineqs; + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(1), y}, {mpq(1), r}}, lconstraint_kind::GE, mpq(9, 10)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(1), y}, {mpq(1), r}}, lconstraint_kind::LE, mpq(11, 10)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(-1), y}, {mpq(1), r}}, lconstraint_kind::GE, mpq(-11, 10)}); + ineqs.push_back(ineq{{{mpq(1), x}, {mpq(-1), y}, {mpq(1), r}}, lconstraint_kind::LE, mpq(1, 10)}); + svector tcols; + setup(solver, ineqs, &tcols); + // a fractional feasible point, so that check() does not return sat right away + solver.set_column_value_test(x, impq(mpq(1, 2))); + solver.set_column_value_test(y, impq(mpq(1, 2))); + solver.set_column_value_test(r, impq(mpq(1, 10))); + solver.set_column_value_test(tcols[0], impq(mpq(11, 10))); + solver.set_column_value_test(tcols[1], impq(mpq(11, 10))); + solver.set_column_value_test(tcols[2], impq(mpq(1, 10))); + solver.set_column_value_test(tcols[3], impq(mpq(1, 10))); + int_solver i_s(solver); + solver.set_int_solver(&i_s); + explanation ex; + lia_move m = i_s.check(&ex); + std::cout << "check returned " << lia_move_to_string(m) + << ", lcube calls: " << solver.settings().stats().m_lcube_calls << "\n"; + VERIFY(m == lia_move::sat); + VERIFY(solver.settings().stats().m_lcube_calls >= 1); + verify_int_values(solver, {x, y}); + verify_model(solver, ineqs); + } + + static void run() { + test_paper_example_undef(); + test_beats_unit_cube(); + test_flip_repair(); + test_infinite_lattice_width(); + test_edge_at_least_one(); + test_dispatch(); + std::cout << "lcube tests passed\n"; + } +} // namespace lcube_test +} // namespace lp + +void tst_lcube() { + lp::lcube_test::run(); +} diff --git a/src/test/lp/lp.cpp b/src/test/lp/lp.cpp index 7a2b27155a..7eaef8543a 100644 --- a/src/test/lp/lp.cpp +++ b/src/test/lp/lp.cpp @@ -1645,7 +1645,7 @@ void test_maximize_term() { lia_move lm = i_solver.check(&ex); VERIFY(lm == lia_move::sat); impq term_max; - lp_status st = solver.maximize_term(term_2x_pl_2y, term_max); + lp_status st = solver.maximize_term(term_2x_pl_2y, term_max, /*fix_int_cols*/ true); std::cout << "status = " << lp_status_to_string(st) << std::endl; std::cout << "term_max = " << term_max << std::endl; diff --git a/src/test/main.cpp b/src/test/main.cpp index b6bb32534d..3a3cab41db 100644 --- a/src/test/main.cpp +++ b/src/test/main.cpp @@ -113,6 +113,9 @@ X(api_bug) \ X(api_special_relations) \ X(arith_rewriter) \ + X(range_predicate) \ + X(regex_range_collapse) \ + X(seq_rewriter) \ X(check_assumptions) \ X(smt_context) \ X(theory_dl) \ @@ -137,6 +140,7 @@ X(zstring) #define FOR_EACH_EXTRA_TEST(X, X_ARGV) \ + X(tptp) \ X(ext_numeral) \ X(interval) \ X(value_generator) \ @@ -190,18 +194,15 @@ X(sls_test) \ X(scoped_vector) \ X(sls_seq_plugin) \ - X(seq_nielsen) \ - X(seq_parikh) \ - X(nseq_basic) \ - X(seq_regex) \ - X(nseq_zipt) \ - X(euf_sgraph) \ - X(euf_seq_plugin) \ X(ho_matcher) \ X(finite_set) \ X(finite_set_rewriter) \ X(seq_split) \ - X(fpa) + X(fpa) \ + X(seq_regex_bisim) \ + X(term_enumeration) \ + X(lcube) \ + X(psmt) #define FOR_EACH_TEST(X, X_ARGV) \ FOR_EACH_ALL_TEST(X, X_ARGV) \ diff --git a/src/test/memory.cpp b/src/test/memory.cpp index 9a8d12b5dd..58e8855b64 100644 --- a/src/test/memory.cpp +++ b/src/test/memory.cpp @@ -39,7 +39,7 @@ static void hit_me(char const* wm) { Z3_mk_bv_sort(ctx,i); } - catch (std::bad_alloc) { + catch (const std::bad_alloc&) { std::cout << "caught\n"; } } diff --git a/src/test/model_evaluator.cpp b/src/test/model_evaluator.cpp index f4a64d4dc8..571b229c49 100644 --- a/src/test/model_evaluator.cpp +++ b/src/test/model_evaluator.cpp @@ -1,5 +1,6 @@ #include "model/model.h" #include "model/model_evaluator.h" +#include "model/func_interp.h" #include "model/model_pp.h" #include "ast/arith_decl_plugin.h" #include "ast/reg_decl_plugins.h" @@ -65,5 +66,29 @@ void tst_model_evaluator() { eval(e, v); std::cout << e << " " << v << "\n"; } + + { + func_interp fi2(m, 1); + expr_ref zero(a.mk_int(0), m); + expr_ref one(a.mk_int(1), m); + fi2.set_else(zero); + for (unsigned i = 0; i < 600; ++i) { + expr_ref arg(a.mk_int(rational(i)), m); + expr* args[1] = { arg.get() }; + fi2.insert_entry(args, i == 599 ? one.get() : zero.get()); + } + fi2.compress(); + SASSERT(fi2.num_entries() == 1); + + expr_ref removed_arg(a.mk_int(0), m); + [[maybe_unused]] expr* removed_args[1] = { removed_arg.get() }; + SASSERT(fi2.get_entry(removed_args) == nullptr); + + expr_ref kept_arg(a.mk_int(599), m); + expr* kept_args[1] = { kept_arg.get() }; + [[maybe_unused]] func_entry* kept = fi2.get_entry(kept_args); + SASSERT(kept != nullptr); + SASSERT(kept->get_result() == one.get()); + } } diff --git a/src/test/nlsat.cpp b/src/test/nlsat.cpp index 9709a36bf8..ab2749c3a6 100644 --- a/src/test/nlsat.cpp +++ b/src/test/nlsat.cpp @@ -452,7 +452,7 @@ static void project(nlsat::solver& s, nlsat::explain& ex, nlsat::var x, unsigned std::cout << "\n"; } -static void project_fa(nlsat::solver& s, nlsat::explain& ex, nlsat::var x, unsigned num, nlsat::literal const* lits) { +static nlsat::scoped_literal_vector project_fa(nlsat::solver& s, nlsat::explain& ex, nlsat::var x, unsigned num, nlsat::literal const* lits) { std::cout << "Project "; nlsat::scoped_literal_vector result(s); ex.compute_conflict_explanation(num, lits, result); @@ -464,6 +464,7 @@ static void project_fa(nlsat::solver& s, nlsat::explain& ex, nlsat::var x, unsig s.display(std::cout << " ", ~lits[i]); } std::cout << ")\n"; + return result; } static nlsat::literal mk_gt(nlsat::solver& s, nlsat::poly* p) { @@ -490,6 +491,39 @@ static nlsat::literal mk_root_eq(nlsat::solver& s, nlsat::poly* p, nlsat::var x, return nlsat::literal(b, false); } +static bool contains_var(nlsat::var_vector const& vars, nlsat::var x) { + for (auto v : vars) { + if (v == x) + return true; + } + return false; +} + +static bool clause_contains_root_dependency( + nlsat::solver& s, + nlsat::scoped_literal_vector const& result, + nlsat::atom::kind kind, + nlsat::var target, + unsigned root_index, + nlsat::var dep1, + nlsat::var dep2, + nlsat::var dep3) { + nlsat::pmanager& pm = s.pm(); + nlsat::var_vector vars; + for (auto l : result) { + nlsat::atom* a = s.bool_var2atom(l.var()); + if (!a || !a->is_root_atom() || a->get_kind() != kind) + continue; + nlsat::root_atom* ra = nlsat::to_root_atom(a); + if (ra->x() != target || ra->i() != root_index || pm.max_var(ra->p()) != target) + continue; + s.vars(l, vars); + if (contains_var(vars, dep1) && contains_var(vars, dep2) && contains_var(vars, dep3)) + return true; + } + return false; +} + static void set_assignment_value(nlsat::assignment& as, anum_manager& am, nlsat::var v, rational const& val) { scoped_anum tmp(am); am.set(tmp, val.to_mpq()); @@ -1183,8 +1217,8 @@ static void tst_15() { auto cell = lws.single_cell(); } -// Test case for unsound lemma lws2380 - comparing standard projection vs levelwise -// The issue: x7 is unconstrained in levelwise output but affects the section polynomial +// Historical lws2380 regression test: both projection paths should preserve +// the x7-linked section/root constraints that witness the projected dependency. static void tst_16() { // enable_trace("nlsat_explain"); @@ -1283,8 +1317,9 @@ static void tst_16() { lits.push_back(mk_gt(s, p0.get())); // x13 > 0 lits.push_back(mk_gt(s, p1.get())); // p1 > 0 - project_fa(s, ex, x13, lits.size(), lits.data()); + auto result = project_fa(s, ex, x13, lits.size(), lits.data()); std::cout << "\n"; + ENSURE(clause_contains_root_dependency(s, result, nlsat::atom::ROOT_EQ, x11, 1, x7, x8, x10)); }; run_test(false); // Standard projection @@ -2144,11 +2179,11 @@ static void tst_22() { } } - if (all_false) { + if (all_false) std::cout << "*** ALL literals FALSE at counterexample - LEMMA IS UNSOUND! ***\n"; - } else { + else std::cout << "At least one literal is TRUE - lemma is sound at this point\n"; - } + ENSURE(!all_false); }; run_test(false); // lws=false (buggy) diff --git a/src/test/object_allocator.cpp b/src/test/object_allocator.cpp index be07424e21..9f0aa351da 100644 --- a/src/test/object_allocator.cpp +++ b/src/test/object_allocator.cpp @@ -73,7 +73,7 @@ static void tst2() { m.enable_concurrent(true); vector > object_coeff_pairs; - unsigned num_resets = 0; + [[maybe_unused]] unsigned num_resets = 0; for (unsigned i = 0; i < 100000; ++i) { unsigned idx = rand() % 6; diff --git a/src/test/parametric_datatype.cpp b/src/test/parametric_datatype.cpp index 2a31803aa0..005f6a8c96 100644 --- a/src/test/parametric_datatype.cpp +++ b/src/test/parametric_datatype.cpp @@ -76,9 +76,9 @@ static void test_polymorphic_datatype_api() { std::cout << "triple_int_bool: " << Z3_sort_to_string(ctx, triple_int_bool) << "\n"; // Get constructors and accessors from the instantiated datatype - Z3_func_decl mk_triple_int_bool = Z3_get_datatype_sort_constructor(ctx, triple_int_bool, 0); + [[maybe_unused]] Z3_func_decl mk_triple_int_bool = Z3_get_datatype_sort_constructor(ctx, triple_int_bool, 0); Z3_func_decl first_int_bool = Z3_get_datatype_sort_constructor_accessor(ctx, triple_int_bool, 0, 0); - Z3_func_decl second_int_bool = Z3_get_datatype_sort_constructor_accessor(ctx, triple_int_bool, 0, 1); + [[maybe_unused]] Z3_func_decl second_int_bool = Z3_get_datatype_sort_constructor_accessor(ctx, triple_int_bool, 0, 1); Z3_func_decl third_int_bool = Z3_get_datatype_sort_constructor_accessor(ctx, triple_int_bool, 0, 2); std::cout << "Got constructors and accessors from instantiated datatype\n"; diff --git a/src/test/psmt.cpp b/src/test/psmt.cpp new file mode 100644 index 0000000000..c8cf2da541 --- /dev/null +++ b/src/test/psmt.cpp @@ -0,0 +1,153 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + psmt.cpp + +Abstract: + + Tests for the parallel SMT tactic (psmt). + Regression test for GitHub issue #9985 (deadlock on psmt). + +--*/ +#ifndef SINGLE_THREAD + +#include "ast/reg_decl_plugins.h" +#include "ast/array_decl_plugin.h" +#include "ast/arith_decl_plugin.h" +#include "smt/smt_solver.h" +#include "smt/tactic/smt_tactic_core.h" +#include "tactic/goal.h" +#include "tactic/tactic.h" +#include + +// Regression test for GitHub issue #9985: +// +// psmt used to deadlock when every CDCL worker returned l_undef with a +// reason other than "max-conflicts-reached" (e.g., theory incompleteness). +// batch_manager::set_unknown() changed the terminal state but did not +// notify backbone workers or the core-minimizer worker that were blocked on +// their condition variables. Those workers blocked forever. +// +// Fix: set_unknown() now calls m_bb_cv.notify_all() and +// m_core_min_cv.notify_all() so all waiting helper threads observe the +// state change and exit cleanly. +// +// We exercise three cases: +// 1. SAT – trivially satisfiable boolean formula. +// 2. UNSAT – trivially unsatisfiable boolean formula. +// 3. UNKNOWN (no deadlock) – formula whose root cube is theory-incomplete. +// The formula uses (as-array f) terms for a function f : Int -> Bool. +// The array theory marks itself incomplete for as-array, so every +// CDCL worker returns l_undef with "(incomplete (theory array))", +// triggering the set_unknown path that used to deadlock. +static void tst_psmt_worker() { + ast_manager m; + reg_decl_plugins(m); + params_ref p; + + // ------------------------------------------------------------------ + // 1. SAT test: assert (or x (not x)) – always true + // ------------------------------------------------------------------ + { + tactic_ref t = mk_parallel_smt_tactic(m, p); + goal_ref g = alloc(goal, m, false, true, false); + expr_ref x(m.mk_const(symbol("x"), m.mk_bool_sort()), m); + g->assert_expr(m.mk_or(x, m.mk_not(x))); + + model_ref md; + labels_vec labels; + proof_ref pr(m); + expr_dependency_ref core(m); + std::string reason_unknown; + lbool r = check_sat(*t, g, md, labels, pr, core, reason_unknown); + SASSERT(r == l_true); + (void)r; + std::cout << "psmt SAT: " << r << "\n"; + } + + // ------------------------------------------------------------------ + // 2. UNSAT test: assert (and x (not x)) + // ------------------------------------------------------------------ + { + tactic_ref t = mk_parallel_smt_tactic(m, p); + goal_ref g = alloc(goal, m, false, true, false); + expr_ref x(m.mk_const(symbol("x"), m.mk_bool_sort()), m); + g->assert_expr(m.mk_and(x, m.mk_not(x))); + + model_ref md; + labels_vec labels; + proof_ref pr(m); + expr_dependency_ref core(m); + std::string reason_unknown; + lbool r = check_sat(*t, g, md, labels, pr, core, reason_unknown); + SASSERT(r == l_false); + (void)r; + std::cout << "psmt UNSAT: " << r << "\n"; + } + + // ------------------------------------------------------------------ + // 3. UNKNOWN (deadlock regression) test. + // + // Reproduce: (declare-fun f (Int) Bool) + // (declare-fun g (Int) Bool) + // (assert (distinct f g)) + // (check-sat-using psmt) + // + // In SMT-LIB2, the function symbols f and g are lifted to array terms + // via (as-array f) and (as-array g). The array theory is explicitly + // incomplete for as-array, so check_sat returns l_undef with reason + // "(incomplete (theory array))". Previously set_unknown() forgot to + // notify backbone and core-minimizer workers' condition variables, + // causing a deadlock; now it does. + // ------------------------------------------------------------------ + { + tactic_ref t = mk_parallel_smt_tactic(m, p); + goal_ref g = alloc(goal, m, false, true, false); + + // Build func_decls: f : Int -> Bool, g : Int -> Bool + arith_util arith(m); + sort* int_s = arith.mk_int(); + sort* domain[1] = { int_s }; + func_decl* f_decl = m.mk_func_decl(symbol("f_fn"), 1, domain, m.mk_bool_sort()); + func_decl* g_decl = m.mk_func_decl(symbol("g_fn"), 1, domain, m.mk_bool_sort()); + + // (as-array f) and (as-array g): array representations of f and g + array_util autil(m); + expr_ref f_arr(autil.mk_as_array(f_decl), m); + expr_ref g_arr(autil.mk_as_array(g_decl), m); + + // (distinct (as-array f) (as-array g)) + expr* dist_args[2] = { f_arr, g_arr }; + expr_ref distinct_fg(m.mk_distinct(2, dist_args), m); + g->assert_expr(distinct_fg); + + model_ref md; + labels_vec labels; + proof_ref pr(m); + expr_dependency_ref core(m); + std::string reason_unknown; + lbool r = check_sat(*t, g, md, labels, pr, core, reason_unknown); + // The result must be l_undef (theory-incomplete). + // If the fix is absent, this call deadlocks instead of returning. + SASSERT(r == l_undef); + (void)r; + std::cout << "psmt UNKNOWN (no deadlock): " << r << "\n"; + } + + std::cout << "psmt tests passed\n"; +} + +void tst_psmt() { + tst_psmt_worker(); +} + +#else + +void tst_psmt() { + // Single-threaded build: parallel tactic degrades to sequential. + // No deadlock is possible; nothing to test. +} + +#endif diff --git a/src/test/range_predicate.cpp b/src/test/range_predicate.cpp new file mode 100644 index 0000000000..e526a63302 --- /dev/null +++ b/src/test/range_predicate.cpp @@ -0,0 +1,260 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + test/range_predicate.cpp + +Abstract: + + Unit tests for the range-algebra value type seq::range_predicate. + + The tests exercise: + * factory constructors and canonical-form invariants, + * extensional equality and total ordering, + * Boolean operations (|, &, ~, -, ^) on hand-picked instances, + * exhaustive verification of de-Morgan and lattice laws on a + small character domain, by enumerating every subset. + +Author: + + Margus Veanes (veanes) 2026 + +--*/ + +#include "ast/rewriter/seq_range_predicate.h" +#include "util/debug.h" +#include +#include +#include + +using seq::range_predicate; + +namespace { + + // Build a range_predicate from a bitmask over [0, max_char] for testing. + range_predicate from_mask(uint64_t mask, unsigned max_char) { + range_predicate r = range_predicate::empty(max_char); + for (unsigned c = 0; c <= max_char; ++c) + if ((mask >> c) & 1u) + r = r | range_predicate::singleton(c, max_char); + return r; + } + + // Convert a range_predicate back to a bitmask for cross-checking. + uint64_t to_mask(range_predicate const& r) { + uint64_t mask = 0; + for (unsigned c = 0; c <= r.max_char(); ++c) + if (r.contains(c)) + mask |= (uint64_t(1) << c); + return mask; + } + + void test_factories() { + auto e = range_predicate::empty(255); + ENSURE(e.is_empty()); + ENSURE(!e.is_top()); + ENSURE(e.num_ranges() == 0); + ENSURE(e.cardinality() == 0); + + auto t = range_predicate::top(255); + ENSURE(!t.is_empty()); + ENSURE(t.is_top()); + ENSURE(t.num_ranges() == 1); + ENSURE(t.cardinality() == 256); + ENSURE(t.contains(0)); + ENSURE(t.contains(255)); + + auto s = range_predicate::singleton(42, 255); + ENSURE(s.num_ranges() == 1); + ENSURE(s.cardinality() == 1); + ENSURE(s.contains(42)); + ENSURE(!s.contains(41)); + unsigned c = 0; + ENSURE(s.is_singleton(c)); + ENSURE(c == 42); + + auto r = range_predicate::range(10, 20, 255); + ENSURE(r.num_ranges() == 1); + ENSURE(r.cardinality() == 11); + ENSURE(r.contains(10)); + ENSURE(r.contains(20)); + ENSURE(!r.contains(9)); + ENSURE(!r.contains(21)); + + // Reversed bounds produce empty. + auto bad = range_predicate::range(20, 10, 255); + ENSURE(bad.is_empty()); + + // Clipping at max_char. + auto clipped = range_predicate::range(200, 1000, 255); + ENSURE(clipped.num_ranges() == 1); + ENSURE(clipped[0] == std::make_pair(200u, 255u)); + } + + void test_equality_and_order() { + auto a = range_predicate::range(1, 5, 31); + auto b = range_predicate::range(1, 5, 31); + auto c = range_predicate::range(1, 6, 31); + ENSURE(a == b); + ENSURE(a != c); + ENSURE(a.hash() == b.hash()); + ENSURE(a < c || c < a); + ENSURE(!(a < a)); + + auto empty = range_predicate::empty(31); + ENSURE(empty < a); + + // Canonical merging of adjacent ranges. + auto d = range_predicate::range(0, 4, 31) | range_predicate::range(5, 10, 31); + auto e = range_predicate::range(0, 10, 31); + ENSURE(d == e); + } + + void test_union_intersection_hand() { + unsigned const M = 31; + auto a = range_predicate::range(0, 4, M) | range_predicate::range(10, 14, M); + auto b = range_predicate::range(3, 11, M); + + auto u = a | b; // [0,14] + ENSURE(u.num_ranges() == 1); + ENSURE(u[0] == std::make_pair(0u, 14u)); + + auto i = a & b; // [3,4] U [10,11] + ENSURE(i.num_ranges() == 2); + ENSURE(i[0] == std::make_pair(3u, 4u)); + ENSURE(i[1] == std::make_pair(10u, 11u)); + + auto d = a - b; // [0,2] U [12,14] + ENSURE(d.num_ranges() == 2); + ENSURE(d[0] == std::make_pair(0u, 2u)); + ENSURE(d[1] == std::make_pair(12u, 14u)); + + auto x = a ^ b; // [0,2] U [5,9] U [12,14] + ENSURE(x.num_ranges() == 3); + ENSURE(x[0] == std::make_pair(0u, 2u)); + ENSURE(x[1] == std::make_pair(5u, 9u)); + ENSURE(x[2] == std::make_pair(12u, 14u)); + } + + void test_complement_hand() { + unsigned const M = 10; + auto e = range_predicate::empty(M); + ENSURE((~e).is_top()); + auto t = range_predicate::top(M); + ENSURE((~t).is_empty()); + + // ~([2,3] U [7,8]) = [0,1] U [4,6] U [9,10] + auto a = range_predicate::range(2, 3, M) | range_predicate::range(7, 8, M); + auto na = ~a; + ENSURE(na.num_ranges() == 3); + ENSURE(na[0] == std::make_pair(0u, 1u)); + ENSURE(na[1] == std::make_pair(4u, 6u)); + ENSURE(na[2] == std::make_pair(9u, 10u)); + + // ~([0,4]) = [5,10] + auto b = range_predicate::range(0, 4, M); + auto nb = ~b; + ENSURE(nb.num_ranges() == 1); + ENSURE(nb[0] == std::make_pair(5u, 10u)); + + // ~([5,10]) = [0,4] + auto cnb = ~nb; + ENSURE(cnb == b); + } + + // Exhaustively verify the lattice / de-Morgan laws on a small domain + // by enumerating every possible subset (bitmask). + void test_exhaustive_laws() { + unsigned const M = 5; // 6 characters -> 64 subsets + unsigned const N = 1u << (M + 1); + for (unsigned i = 0; i < N; ++i) { + range_predicate A = from_mask(i, M); + ENSURE(to_mask(A) == i); + // ~ ~ A == A + ENSURE(~~A == A); + // A | ~A == top + ENSURE((A | ~A).is_top()); + // A & ~A == empty + ENSURE((A & ~A).is_empty()); + // cardinality matches popcount + unsigned pop = 0; + for (unsigned k = 0; k <= M; ++k) if ((i >> k) & 1u) ++pop; + ENSURE(A.cardinality() == pop); + } + for (unsigned i = 0; i < N; ++i) { + range_predicate A = from_mask(i, M); + for (unsigned j = 0; j < N; ++j) { + range_predicate B = from_mask(j, M); + // Bitmask reference semantics. + ENSURE(to_mask(A | B) == (i | j)); + ENSURE(to_mask(A & B) == (i & j)); + ENSURE(to_mask(A - B) == (i & ~j & ((1u << (M + 1)) - 1u))); + ENSURE(to_mask(A ^ B) == (i ^ j)); + // de-Morgan + ENSURE(~(A | B) == (~A & ~B)); + ENSURE(~(A & B) == (~A | ~B)); + // Commutativity + ENSURE((A | B) == (B | A)); + ENSURE((A & B) == (B & A)); + // (A - B) == A & ~B + ENSURE((A - B) == (A & ~B)); + // (A ^ B) == (A | B) - (A & B) + ENSURE((A ^ B) == ((A | B) - (A & B))); + // Extensional equality is reflexive on equal masks. + if (i == j) { + ENSURE(A == B); + ENSURE(A.hash() == B.hash()); + } + } + } + } + + void test_total_order_strict() { + unsigned const M = 5; + unsigned const N = 1u << (M + 1); + // Strict total order: for any distinct A, B exactly one of A + +namespace { + + using seq::range_predicate; + using seq::regex_to_range_predicate; + using seq::range_predicate_to_regex; + + static void check(bool ok, char const* what) { + if (!ok) { + std::cerr << "regex_range_collapse FAILED: " << what << "\n"; + ENSURE(false); + } + } + + static expr_ref mk_singleton_str(seq_util& u, unsigned c) { + return expr_ref(u.str.mk_string(zstring(c)), u.get_manager()); + } + + static bool extract_range_chars(seq_util& u, expr* e, unsigned& lo, unsigned& hi) { + expr* lo_e = nullptr; expr* hi_e = nullptr; + expr *s = nullptr; + zstring str; + if (u.re.is_to_re(e, s) && u.str.is_string(s, str) && str.length() == 1) { + lo = hi = str[0]; + return true; + } + else if (u.re.is_range(e, lo_e, hi_e) && u.str.is_string(lo_e) && u.str.is_string(hi_e)) { + zstring lo_str, hi_str; + u.str.is_string(lo_e, lo_str); + u.str.is_string(hi_e, hi_str); + if (lo_str.length() == 1 && hi_str.length() == 1) { + lo = lo_str[0]; + hi = hi_str[0]; + return true; + } + } + if (!u.re.is_range(e, lo_e, hi_e)) + return false; + // Accept either string-constant or (seq.unit (Char N)) bound form. + if (u.re.is_range(e, lo, hi)) + return true; + expr* lc = nullptr; expr* hc = nullptr; + if (u.str.is_unit(lo_e, lc) && u.is_const_char(lc, lo) && + u.str.is_unit(hi_e, hc) && u.is_const_char(hc, hi)) + return true; + return false; + } + + static void run() { + ast_manager m; + reg_decl_plugins(m); + seq_util u(m); + unsigned const M = u.max_char(); + + sort* str_sort = u.str.mk_string_sort(); + sort* re_sort = u.re.mk_re(str_sort); + + // primitives + { + range_predicate p(M); + check(regex_to_range_predicate(u, u.re.mk_empty(re_sort), p) && p.is_empty(), + "re.empty -> empty"); + check(regex_to_range_predicate(u, u.re.mk_full_char(re_sort), p) && p.is_top(), + "re.full_char -> top"); + } + // re.range "a" "z" + { + range_predicate p(M); + expr_ref a = mk_singleton_str(u, 'a'); + expr_ref z = mk_singleton_str(u, 'z'); + expr_ref r(u.re.mk_range(a, z), m); + check(regex_to_range_predicate(u, r, p) && p.num_ranges() == 1 && + p[0].first == 'a' && p[0].second == 'z', + "re.range a z -> [a,z]"); + } + // Disjoint union: (a..z) | (0..9) + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'z')), m); + expr_ref r2(u.re.mk_range(mk_singleton_str(u, '0'), mk_singleton_str(u, '9')), m); + expr_ref un(u.re.mk_union(r1, r2), m); + check(regex_to_range_predicate(u, un, p) && p.num_ranges() == 2, + "(a-z)|(0-9) -> 2 ranges"); + // canonical order: lower lo first + check(p[0].first == '0' && p[0].second == '9' && p[1].first == 'a' && p[1].second == 'z', + "(a-z)|(0-9) ranges in canonical order"); + } + // Overlapping union: (a..c) | (b..f) -> (a..f) + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'c')), m); + expr_ref r2(u.re.mk_range(mk_singleton_str(u, 'b'), mk_singleton_str(u, 'f')), m); + expr_ref un(u.re.mk_union(r1, r2), m); + check(regex_to_range_predicate(u, un, p) && p.num_ranges() == 1 && + p[0].first == 'a' && p[0].second == 'f', + "(a-c)|(b-f) -> (a-f)"); + } + // Adjacent union: (a..c) | (d..f) -> (a..f) (canonical predicate merges adjacent) + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'c')), m); + expr_ref r2(u.re.mk_range(mk_singleton_str(u, 'd'), mk_singleton_str(u, 'f')), m); + expr_ref un(u.re.mk_union(r1, r2), m); + check(regex_to_range_predicate(u, un, p) && p.num_ranges() == 1 && + p[0].first == 'a' && p[0].second == 'f', + "(a-c)|(d-f) -> (a-f) via adjacency"); + } + // Disjoint intersection: (a..z) & (0..9) -> empty + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'z')), m); + expr_ref r2(u.re.mk_range(mk_singleton_str(u, '0'), mk_singleton_str(u, '9')), m); + expr_ref ix(u.re.mk_inter(r1, r2), m); + check(regex_to_range_predicate(u, ix, p) && p.is_empty(), + "(a-z)&(0-9) -> empty"); + } + // Overlapping intersection: (a..f) & (c..z) -> (c..f) + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'f')), m); + expr_ref r2(u.re.mk_range(mk_singleton_str(u, 'c'), mk_singleton_str(u, 'z')), m); + expr_ref ix(u.re.mk_inter(r1, r2), m); + check(regex_to_range_predicate(u, ix, p) && p.num_ranges() == 1 && + p[0].first == 'c' && p[0].second == 'f', + "(a-f)&(c-z) -> (c-f)"); + } + // Complement: re.complement is intentionally NOT a char-class op + // (it operates over Σ*), so it must NOT be translated. + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'z')), m); + expr_ref cmp(u.re.mk_complement(r1), m); + check(!regex_to_range_predicate(u, cmp, p), + "re.comp of range is NOT translatable (sequence-level complement)"); + } + // Diff: (a..f) \ (c..z) -> (a..b) + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'f')), m); + expr_ref r2(u.re.mk_range(mk_singleton_str(u, 'c'), mk_singleton_str(u, 'z')), m); + expr_ref df(u.re.mk_diff(r1, r2), m); + check(regex_to_range_predicate(u, df, p) && p.num_ranges() == 1 && + p[0].first == 'a' && p[0].second == 'b', + "(a-f) \\ (c-z) -> (a-b)"); + } + // Negative: re.* of a range is NOT a char class + { + range_predicate p(M); + expr_ref r1(u.re.mk_range(mk_singleton_str(u, 'a'), mk_singleton_str(u, 'z')), m); + expr_ref star(u.re.mk_star(r1), m); + check(!regex_to_range_predicate(u, star, p), + "re.* of range not translatable"); + } + + // Negative: a regex whose element type is NOT a sequence of + // characters (here (Seq Int)) must be rejected outright, even for + // shapes that structurally resemble char-class operators. + { + range_predicate p(M); + arith_util a(m); + sort* int_seq = u.str.mk_seq(a.mk_int()); + sort* int_re = u.re.mk_re(int_seq); + check(!regex_to_range_predicate(u, u.re.mk_empty(int_re), p), + "re.empty over (Seq Int) is NOT a char class"); + check(!regex_to_range_predicate(u, u.re.mk_full_char(int_re), p), + "re.full_char over (Seq Int) is NOT a char class"); + } + + // ---- materialization round-trip ---- + + // empty -> re.empty + { + range_predicate p = range_predicate::empty(M); + expr_ref e = range_predicate_to_regex(u, p, str_sort); + check(u.re.is_empty(e), "empty -> re.empty"); + } + // top -> re.full_char + { + range_predicate p = range_predicate::top(M); + expr_ref e = range_predicate_to_regex(u, p, str_sort); + check(u.re.is_full_char(e), "top -> re.full_char"); + } + // single range -> re.range + { + range_predicate p = range_predicate::range('a', 'z', M); + expr_ref e = range_predicate_to_regex(u, p, str_sort); + unsigned lo = 0, hi = 0; + check(extract_range_chars(u, e, lo, hi) && lo == 'a' && hi == 'z', + "[a-z] -> re.range a z"); + } + // singleton -> re.range c c + { + range_predicate p = range_predicate::singleton('A', M); + expr_ref e = range_predicate_to_regex(u, p, str_sort); + unsigned lo = 0, hi = 0; + check(extract_range_chars(u, e, lo, hi) && lo == 'A' && hi == 'A', + "{A} -> re.range A A"); + } + // 2 ranges -> re.union(range_0, range_1) in canonical order + { + range_predicate p = range_predicate::range('0', '9', M) + | range_predicate::range('a', 'z', M); + expr_ref e = range_predicate_to_regex(u, p, str_sort); + expr* a = nullptr; expr* b = nullptr; + check(u.re.is_union(e, a, b), "2-range -> union"); + unsigned lo0 = 0, hi0 = 0, lo1 = 0, hi1 = 0; + check(extract_range_chars(u, a, lo0, hi0) && lo0 == '0' && hi0 == '9', + "union arg0 = (0-9) (canonical: lower lo first)"); + check(extract_range_chars(u, b, lo1, hi1) && lo1 == 'a' && hi1 == 'z', + "union arg1 = (a-z)"); + } + // 3 ranges -> right-associated union + { + range_predicate p = range_predicate::range(0, 5, M) + | range_predicate::range(10, 15, M) + | range_predicate::range(20, 25, M); + expr_ref e = range_predicate_to_regex(u, p, str_sort); + expr* a = nullptr; expr* rest = nullptr; + check(u.re.is_union(e, a, rest), "3-range -> union(...)"); + unsigned lo = 0, hi = 0; + check(extract_range_chars(u, a, lo, hi) && lo == 0 && hi == 5, "first arg = (0-5)"); + expr* b = nullptr; expr* c = nullptr; + check(u.re.is_union(rest, b, c), "rest is union(...,...)"); + check(extract_range_chars(u, b, lo, hi) && lo == 10 && hi == 15, "second range"); + check(extract_range_chars(u, c, lo, hi) && lo == 20 && hi == 25, "third range"); + } + // Round-trip identity for an arbitrary range-set + { + range_predicate p_in = range_predicate::range('a', 'c', M) + | range_predicate::range('m', 'p', M) + | range_predicate::range('x', 'z', M); + expr_ref e = range_predicate_to_regex(u, p_in, str_sort); + range_predicate p_out(M); + check(regex_to_range_predicate(u, e, p_out), "round-trip translatable"); + check(p_in == p_out, "round-trip equal"); + } + + std::cerr << "regex_range_collapse tests passed\n"; + } +} + +void tst_regex_range_collapse() { + run(); +} diff --git a/src/test/seq_regex_bisim.cpp b/src/test/seq_regex_bisim.cpp new file mode 100644 index 0000000000..49a96e8c20 --- /dev/null +++ b/src/test/seq_regex_bisim.cpp @@ -0,0 +1,127 @@ +// Regression test for the seq::derive::intersect_intervals bug. +// +// Background: derive uses a path-tracking interval set to compute symbolic +// derivatives. The intersect_intervals routine used to react to a single +// disjoint interval by dropping the entire kept suffix and skipping the rest +// of the list, which silently killed valid branches in derivatives such as +// D(a|b). That made the bisimulation procedure conclude bogus equalities +// like a* == (a|b)*. +// +// This file also covers the seq::derive top-level-cache poisoning bug. +// `m_top_cache` is keyed only by the regex; the routine used to populate it +// while `m_ele` was set to a *concrete* character, baking that character +// into the cached "symbolic" derivative. Subsequent calls with the same +// regex but a different ele then returned a stale concrete answer instead +// of the true symbolic derivative. The simplest victim is +// (str.in_re "aP" (re.++ (re.* "a") "P")) +// which used to return false because the derivative wrt 'a' was cached and +// re-used as the derivative wrt 'P'. +#include "ast/ast.h" +#include "ast/ast_pp.h" +#include "ast/reg_decl_plugins.h" +#include "ast/seq_decl_plugin.h" +#include "ast/rewriter/seq_rewriter.h" +#include "ast/rewriter/seq_regex_bisim.h" +#include "ast/rewriter/th_rewriter.h" +#include + +static void test_a_star_neq_ab_star() { + ast_manager m; + reg_decl_plugins(m); + seq_util u(m); + seq_rewriter rw(m); + + sort_ref str_sort(u.str.mk_string_sort(), m); + + zstring sa("a"), sb("b"); + expr_ref re_a(u.re.mk_to_re(u.str.mk_string(sa)), m); + expr_ref re_b(u.re.mk_to_re(u.str.mk_string(sb)), m); + expr_ref a_star(u.re.mk_star(re_a), m); + expr_ref ab(u.re.mk_union(re_a, re_b), m); + expr_ref ab_star(u.re.mk_star(ab), m); + + expr_ref d_ab = rw.mk_brz_derivative(ab); + std::cout << "D(a|b) = " << mk_pp(d_ab, m) << "\n"; + + // Both the 'a' branch and the 'b' branch of D(a|b) must reach epsilon. + // Collect the regex leaves of the symbolic derivative and require at + // least two distinct accepting leaves (one for 'a' and one for 'b'). + expr_ref_vector leaves(m); + auto collect = [&](expr* e, auto&& self) -> void { + expr* c, *t, *f; + if (m.is_ite(e, c, t, f) || u.re.is_union(e, t, f)) { + self(t, self); + self(f, self); + return; + } + if (u.re.is_empty(e)) return; + leaves.push_back(e); + }; + collect(d_ab, collect); + unsigned nullable_leaves = 0; + for (expr* l : leaves) { + expr_ref n = rw.is_nullable(l); + if (m.is_true(n)) ++nullable_leaves; + } + std::cout << "D(a|b) leaves=" << leaves.size() + << " nullable=" << nullable_leaves << "\n"; + ENSURE(nullable_leaves >= 2); + + // Bisim must report the two languages are not equivalent. + seq::regex_bisim bisim(rw); + lbool eq = bisim.are_equivalent(a_star, ab_star); + std::cout << "bisim(a*, (a|b)*) = " + << (eq == l_true ? "true" : eq == l_false ? "false" : "undef") << "\n"; + ENSURE(eq == l_false); +} + +// Regression for the derive top-level-cache poisoning bug. +// Take r = (re.* "a") ++ "P" and check str.in_re "aP" r. Before the fix +// the first per-char derivative call (wrt 'a') populated m_top_cache with +// 'a' baked into the symbolic ITE-tree, so the next call (wrt 'P') returned +// that stale cached value instead of computing D_P(r) = epsilon, making +// str.in_re wrongly return false. +static void test_derive_cache_per_ele() { + ast_manager m; + reg_decl_plugins(m); + seq_util u(m); + seq_rewriter rw(m); + + sort_ref str_sort(u.str.mk_string_sort(), m); + + zstring sa("a"), sP("P"), s_aP("aP"); + expr_ref re_a(u.re.mk_to_re(u.str.mk_string(sa)), m); + expr_ref re_P(u.re.mk_to_re(u.str.mk_string(sP)), m); + expr_ref a_star(u.re.mk_star(re_a), m); + expr_ref r(u.re.mk_concat(a_star, re_P), m); + expr_ref aP(u.str.mk_string(s_aP), m); + + // Compute D_'a'(a*P) and D_'P'(a*P) directly via mk_derivative. + // Before the fix, m_top_cache was populated while m_ele = ele (the + // concrete char), so the second call hit the stale cached answer from + // the first. After the fix the cache is keyed by a symbolic var, so + // each concrete-ele substitution produces the right answer. + expr_ref ch_a(u.mk_char('a'), m); + expr_ref ch_P(u.mk_char('P'), m); + expr_ref d_a = rw.mk_derivative(ch_a, r); + expr_ref d_P = rw.mk_derivative(ch_P, r); + std::cout << "D_a(a*P) = " << mk_pp(d_a, m) << "\n"; + std::cout << "D_P(a*P) = " << mk_pp(d_P, m) << "\n"; + + // D_P(a*P) must be nullable (it accepts the empty suffix), while + // D_a(a*P) must not be (it still needs a trailing 'P'). + expr_ref n_a = rw.is_nullable(d_a); + expr_ref n_P = rw.is_nullable(d_P); + th_rewriter trw(m); + trw(n_a); + trw(n_P); + std::cout << "nullable(D_a) = " << mk_pp(n_a, m) << "\n"; + std::cout << "nullable(D_P) = " << mk_pp(n_P, m) << "\n"; + ENSURE(m.is_false(n_a)); + ENSURE(m.is_true(n_P)); +} + +void tst_seq_regex_bisim() { + test_a_star_neq_ab_star(); + test_derive_cache_per_ele(); +} diff --git a/src/test/seq_rewriter.cpp b/src/test/seq_rewriter.cpp new file mode 100644 index 0000000000..658675fa72 --- /dev/null +++ b/src/test/seq_rewriter.cpp @@ -0,0 +1,171 @@ +/*++ +Copyright (c) 2024 Microsoft Corporation + +Regression tests for seq_rewriter smart constructors for regex ranges. + +Tests: + 1. Empty range (lo > hi) → re.none + 2. Singleton range (lo == hi) → str.to_re lo + 3. Range ∩ Range → reduced range or re.none + 4. Range ∪ Range → merged range for overlapping/adjacent + 5. Complement of range → one or two ranges + 6. Downstream operators absorb empty ranges correctly +--*/ + +#include "ast/ast_pp.h" +#include "ast/reg_decl_plugins.h" +#include "ast/rewriter/th_rewriter.h" +#include "ast/seq_decl_plugin.h" +#include + +// Build a single-char string literal expression. +static expr_ref mk_str(ast_manager& m, seq_util& su, unsigned c) { + return expr_ref(su.str.mk_string(zstring(c)), m); +} + +void tst_seq_rewriter() { + ast_manager m; + reg_decl_plugins(m); + th_rewriter rw(m); + seq_util su(m); + + sort* str_sort = su.str.mk_string_sort(); + sort* re_sort = su.re.mk_re(str_sort); + + auto range = [&](unsigned lo, unsigned hi) -> expr_ref { + return expr_ref(su.re.mk_range(mk_str(m, su, lo), mk_str(m, su, hi)), m); + }; + + // Arbitrary regex variable for downstream tests. + app_ref R(m.mk_fresh_const("R", re_sort), m); + + // ----------------------------------------------------------------------- + // 1. Empty range (lo > hi) → re.none + // ----------------------------------------------------------------------- + { + expr_ref e = range('z', 'a'); + rw(e); + std::cout << "empty range lo>hi: " << mk_pp(e, m) << "\n"; + ENSURE(su.re.is_empty(e)); + } + + // ----------------------------------------------------------------------- + // 2. Singleton range (lo == hi) → str.to_re lo + // ----------------------------------------------------------------------- + { + expr_ref e = range('a', 'a'); + rw(e); + std::cout << "singleton range: " << mk_pp(e, m) << "\n"; + expr* inner = nullptr; + ENSURE(su.re.is_to_re(e, inner)); + } + + // ----------------------------------------------------------------------- + // 3. Range intersection: overlapping → smaller range + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_inter(range('a', 'z'), range('f', 'k')), m); + rw(e); + std::cout << "range inter overlapping: " << mk_pp(e, m) << "\n"; + unsigned lo = 0, hi = 0; + ENSURE(su.re.is_range(e, lo, hi) && lo == 'f' && hi == 'k'); + } + + // ----------------------------------------------------------------------- + // 4. Range intersection: disjoint → re.none + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_inter(range('a', 'f'), range('k', 'z')), m); + rw(e); + std::cout << "range inter disjoint: " << mk_pp(e, m) << "\n"; + ENSURE(su.re.is_empty(e)); + } + + // ----------------------------------------------------------------------- + // 5. Range intersection: touching at boundary → singleton (str.to_re "f") + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_inter(range('a', 'f'), range('f', 'z')), m); + rw(e); + std::cout << "range inter touching: " << mk_pp(e, m) << "\n"; + expr* inner = nullptr; + ENSURE(su.re.is_to_re(e, inner)); + } + + // ----------------------------------------------------------------------- + // 6. Range union: overlapping → merged range + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_union(range('a', 'f'), range('e', 'k')), m); + rw(e); + std::cout << "range union overlapping: " << mk_pp(e, m) << "\n"; + unsigned lo = 0, hi = 0; + ENSURE(su.re.is_range(e, lo, hi) && lo == 'a' && hi == 'k'); + } + + // ----------------------------------------------------------------------- + // 7. Range union: adjacent → merged range + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_union(range('a', 'f'), range('g', 'k')), m); + rw(e); + std::cout << "range union adjacent: " << mk_pp(e, m) << "\n"; + unsigned lo = 0, hi = 0; + ENSURE(su.re.is_range(e, lo, hi) && lo == 'a' && hi == 'k'); + } + + // ----------------------------------------------------------------------- + // 8. Range union: disjoint → stays as union + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_union(range('a', 'c'), range('m', 'z')), m); + rw(e); + std::cout << "range union disjoint (stays as union): " << mk_pp(e, m) << "\n"; + ENSURE(!su.re.is_range(e)); + } + + + // ----------------------------------------------------------------------- + // 11. Downstream: (re.* (re.range "z" "a")) → str.to_re "" + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_star(range('z', 'a')), m); + rw(e); + std::cout << "star of empty range: " << mk_pp(e, m) << "\n"; + expr* inner = nullptr; + // star of empty → epsilon (str.to_re "") + ENSURE(su.re.is_to_re(e, inner) && su.str.is_empty(inner)); + } + + // ----------------------------------------------------------------------- + // 12. Downstream: concat absorbs empty range → re.none + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_concat(R, su.re.mk_concat(range('z', 'a'), R)), m); + rw(e); + std::cout << "concat absorbs empty range: " << mk_pp(e, m) << "\n"; + ENSURE(su.re.is_empty(e)); + } + + // ----------------------------------------------------------------------- + // 13. Downstream: union absorbs empty range → R + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_union(R, range('z', 'a')), m); + rw(e); + std::cout << "union absorbs empty range: " << mk_pp(e, m) << "\n"; + ENSURE(e.get() == R.get()); + } + + // ----------------------------------------------------------------------- + // 14. Downstream: inter absorbs empty range → re.none + // ----------------------------------------------------------------------- + { + expr_ref e(su.re.mk_inter(R, range('z', 'a')), m); + rw(e); + std::cout << "inter absorbs empty range: " << mk_pp(e, m) << "\n"; + ENSURE(su.re.is_empty(e)); + } + + std::cout << "tst_seq_rewriter: all tests passed\n"; +} diff --git a/src/test/sls_seq_plugin.cpp b/src/test/sls_seq_plugin.cpp index 4c8c5340c6..94d1ce5bb9 100644 --- a/src/test/sls_seq_plugin.cpp +++ b/src/test/sls_seq_plugin.cpp @@ -199,7 +199,7 @@ struct test_seq { ptr_vector const& lhs(expr* eq) { auto& ev = get_eval(eq); if (ev.lhs.empty()) { - expr* x, * y; + expr* x = nullptr, * y = nullptr; VERIFY(m.is_eq(eq, x, y)); seq.str.get_concat(x, ev.lhs); seq.str.get_concat(y, ev.rhs); diff --git a/src/test/sls_test.cpp b/src/test/sls_test.cpp index 2ca2948d52..18955fe825 100644 --- a/src/test/sls_test.cpp +++ b/src/test/sls_test.cpp @@ -239,7 +239,7 @@ namespace bv { } -static void test_eval1() { +[[maybe_unused]] static void test_eval1() { ast_manager m; reg_decl_plugins(m); bv_util bv(m); @@ -262,7 +262,7 @@ static void test_eval1() { } } -static void test_repair1() { +[[maybe_unused]] static void test_repair1() { ast_manager m; reg_decl_plugins(m); bv_util bv(m); diff --git a/src/test/smt_context.cpp b/src/test/smt_context.cpp index d7297c9d0a..215f3285cc 100644 --- a/src/test/smt_context.cpp +++ b/src/test/smt_context.cpp @@ -6,6 +6,7 @@ Copyright (c) 2015 Microsoft Corporation #include "smt/smt_context.h" #include "ast/reg_decl_plugins.h" +#include "ast/arith_decl_plugin.h" void tst_smt_context() { @@ -34,4 +35,31 @@ void tst_smt_context() } ctx.check(); + + { + arith_util a(m); + expr_ref x(m.mk_var(2, a.mk_int()), m); + expr_ref x4(m.mk_var(1, a.mk_int()), m); + expr_ref y(m.mk_var(0, a.mk_int()), m); + expr_ref zero(a.mk_int(0), m); + expr_ref two(a.mk_int(2), m); + expr_ref_vector conjs(m); + conjs.push_back(a.mk_gt(x, y)); + conjs.push_back(a.mk_gt(zero, x4)); + conjs.push_back(a.mk_gt(zero, a.mk_uminus(y))); + conjs.push_back(a.mk_lt(zero, a.mk_uminus(a.mk_mul(two, y)))); + expr_ref body(m.mk_and(conjs), m); + + sort* y_sort = a.mk_int(); + symbol y_name("y"); + body = m.mk_exists(1, &y_sort, &y_name, body); + + sort* sorts[2] = { a.mk_int(), a.mk_int() }; + symbol names[2] = { symbol("x"), symbol("x4") }; + expr_ref q(m.mk_forall(2, sorts, names, body), m); + + smt::context qctx(m, params); + qctx.assert_expr(q); + VERIFY(l_false == qctx.check()); + } } diff --git a/src/test/term_enumeration.cpp b/src/test/term_enumeration.cpp new file mode 100644 index 0000000000..57b5da852f --- /dev/null +++ b/src/test/term_enumeration.cpp @@ -0,0 +1,309 @@ +/*++ +Copyright (c) 2024 Microsoft Corporation + +Module Name: + + tst_term_enumeration.cpp + +Abstract: + + Test term enumeration module + +--*/ + + +#include "ast/rewriter/term_enumeration.h" +#include "ast/ast_pp.h" +#include "ast/arith_decl_plugin.h" +#include "ast/bv_decl_plugin.h" +#include "ast/array_decl_plugin.h" +#include "ast/reg_decl_plugins.h" +#include "ast/rewriter/th_rewriter.h" +#include "util/obj_hashtable.h" +#include +#include + +static void tst_basic_enumeration() { + std::cout << "=== test basic enumeration ===\n"; + ast_manager m; + reg_decl_plugins(m); + arith_util a(m); + + term_enumeration te(m); + + // Add some leaf productions (constants) + expr_ref zero(a.mk_int(0), m); + expr_ref one(a.mk_int(1), m); + te.add_production(zero); + te.add_production(one); + + // Enumerate terms of Int sort + sort* int_sort = a.mk_int(); + unsigned count = 0; + for (expr* e : te.enum_terms(int_sort)) { + std::cout << "Term: " << mk_pp(e, m) << "\n"; + count++; + if (count >= 5) break; // Limit output + } + + ENSURE(count >= 2); // At least 0 and 1 + std::cout << "Enumerated " << count << " terms\n"; +} + +static void tst_enumeration_with_operators() { + std::cout << "=== test enumeration with operators ===\n"; + ast_manager m; + reg_decl_plugins(m); + arith_util a(m); + + term_enumeration te(m); + + // Add leaf productions + expr_ref zero(a.mk_int(0), m); + expr_ref one(a.mk_int(1), m); + te.add_production(zero); + te.add_production(one); + + // Add operator productions (+ and *) + // Get func_decl by creating an app and extracting the decl + app_ref tmp_add(a.mk_add(zero, one), m); + app_ref tmp_mul(a.mk_mul(zero, one), m); + func_decl* add_decl = tmp_add->get_decl(); + func_decl* mul_decl = tmp_mul->get_decl(); + te.add_production(add_decl); + te.add_production(mul_decl); + + sort* int_sort = a.mk_int(); + unsigned count = 0; + for (expr* e : te.enum_terms(int_sort)) { + std::cout << "Term: " << mk_pp(e, m) << "\n"; + count++; + if (count >= 20) break; // Limit output + } + + ENSURE(count >= 2); // At least the leaves + std::cout << "Enumerated " << count << " terms with operators\n"; +} + +static void tst_observational_equivalence_filter() { + std::cout << "=== test observational equivalence filter ===\n"; + ast_manager m; + reg_decl_plugins(m); + arith_util a(m); + th_rewriter rw(m); + + term_enumeration te(m); + + expr_ref zero(a.mk_int(0), m); + expr_ref one(a.mk_int(1), m); + te.add_production(zero); + te.add_production(one); + + app_ref tmp_add(a.mk_add(zero, one), m); + te.add_production(tmp_add->get_decl()); + + sort* int_sort = a.mk_int(); + obj_hashtable seen; + unsigned count = 0; + for (expr* e : te.enum_terms(int_sort)) { + expr_ref r(m); + rw(e, r); + ENSURE(r == e); + ENSURE(!seen.contains(r)); + seen.insert(r); + count++; + if (count >= 20) break; + } + + ENSURE(count >= 2); +} + +static void tst_display() { + std::cout << "=== test display ===\n"; + ast_manager m; + reg_decl_plugins(m); + arith_util a(m); + + term_enumeration te(m); + + // Add leaf productions + expr_ref zero(a.mk_int(0), m); + expr_ref one(a.mk_int(1), m); + te.add_production(zero); + te.add_production(one); + + // Add operator productions + app_ref tmp_add(a.mk_add(zero, one), m); + func_decl* add_decl = tmp_add->get_decl(); + te.add_production(add_decl); + + sort* int_sort = a.mk_int(); + unsigned count = 0; + for (expr* e : te.enum_terms(int_sort)) { + (void)e; + count++; + if (count >= 10) break; + } + + std::cout << "Internal state after enumeration:\n"; + std::ostringstream oss; + te.display(oss); + std::cout << oss.str(); + + // Verify display produced some output + ENSURE(!oss.str().empty()); +} + +static void tst_bitvector_enumeration() { + std::cout << "=== test bitvector enumeration ===\n"; + ast_manager m; + reg_decl_plugins(m); + bv_util bv(m); + + term_enumeration te(m); + + // Add bitvector constants + unsigned bv_size = 8; + expr_ref bv_zero(bv.mk_numeral(0, bv_size), m); + expr_ref bv_one(bv.mk_numeral(1, bv_size), m); + te.add_production(bv_zero); + te.add_production(bv_one); + + // Add bvadd operator + app_ref tmp_add(bv.mk_bv_add(bv_zero, bv_one), m); + func_decl* bvadd = tmp_add->get_decl(); + te.add_production(bvadd); + + sort* bv8 = bv.mk_sort(bv_size); + unsigned count = 0; + for (expr* e : te.enum_terms(bv8)) { + std::cout << "BV Term: " << mk_pp(e, m) << "\n"; + count++; + if (count >= 10) break; + } + + ENSURE(count >= 2); + std::cout << "Enumerated " << count << " bitvector terms\n"; +} + +static void tst_multiple_sorts() { + std::cout << "=== test multiple sorts ===\n"; + ast_manager m; + reg_decl_plugins(m); + arith_util a(m); + + term_enumeration te(m); + + // Add Int constants + expr_ref i_zero(a.mk_int(0), m); + expr_ref i_one(a.mk_int(1), m); + te.add_production(i_zero); + te.add_production(i_one); + + // Add Real constants + expr_ref r_zero(a.mk_real(0), m); + expr_ref r_one(a.mk_real(1), m); + te.add_production(r_zero); + te.add_production(r_one); + + // Enumerate Int terms + sort* int_sort = a.mk_int(); + unsigned int_count = 0; + for (expr* e : te.enum_terms(int_sort)) { + std::cout << "Int Term: " << mk_pp(e, m) << "\n"; + int_count++; + if (int_count >= 5) break; + } + + ENSURE(int_count >= 2); + std::cout << "Enumerated " << int_count << " Int terms\n"; +} + +static void tst_nested_array_enumeration() { + std::cout << "=== test nested array enumeration (Array(A, Array(B, A))) ===\n"; + ast_manager m; + reg_decl_plugins(m); + array_util arr(m); + + term_enumeration te(m); + + // Create uninterpreted sorts A and B + sort_ref sort_A(m.mk_uninterpreted_sort(symbol("A")), m); + sort_ref sort_B(m.mk_uninterpreted_sort(symbol("B")), m); + + // Create nested array sort: Array(B, A) - arrays indexed by B returning A + sort_ref array_B_A(arr.mk_array_sort(sort_B, sort_A), m); + + // Create outer array sort: Array(A, Array(B, A)) - arrays indexed by A returning Array(B,A) + sort_ref array_A_arrayBA(arr.mk_array_sort(sort_A, array_B_A), m); + + std::cout << "Sort A: " << mk_pp(sort_A.get(), m) << "\n"; + std::cout << "Sort B: " << mk_pp(sort_B.get(), m) << "\n"; + std::cout << "Sort Array(B, A): " << mk_pp(array_B_A.get(), m) << "\n"; + std::cout << "Sort Array(A, Array(B, A)): " << mk_pp(array_A_arrayBA.get(), m) << "\n"; + + // Add constants of sort A + app_ref a0(m.mk_const(symbol("a0"), sort_A), m); + app_ref a1(m.mk_const(symbol("a1"), sort_A), m); + te.add_production(a0); + te.add_production(a1); + + // Add constants of sort B + app_ref b0(m.mk_const(symbol("b0"), sort_B), m); + app_ref b1(m.mk_const(symbol("b1"), sort_B), m); + te.add_production(b0); + te.add_production(b1); + + // Add a constant array of inner type Array(B, A) - const_array(a0) : Array(B, A) + app_ref const_inner(arr.mk_const_array(array_B_A, a0), m); + te.add_production(const_inner); + + // Add a constant array of outer type Array(A, Array(B, A)) + app_ref const_outer(arr.mk_const_array(array_A_arrayBA, const_inner), m); + te.add_production(const_outer); + + // Add store operator for the inner array type Array(B, A) + // store(array, index, value) : store(Array(B,A), B, A) -> Array(B,A) + expr* store_inner_args[3] = { const_inner.get(), b0.get(), a0.get() }; + app_ref tmp_store_inner(arr.mk_store(3, store_inner_args), m); + func_decl* store_inner_decl = tmp_store_inner->get_decl(); + te.add_production(store_inner_decl); + + // Add store operator for the outer array type Array(A, Array(B, A)) + // store(array, index, value) : store(Array(A, Array(B,A)), A, Array(B,A)) -> Array(A, Array(B,A)) + expr* store_outer_args[3] = { const_outer.get(), a0.get(), const_inner.get() }; + app_ref tmp_store_outer(arr.mk_store(3, store_outer_args), m); + func_decl* store_outer_decl = tmp_store_outer->get_decl(); + te.add_production(store_outer_decl); + + // Add select operator for the outer array (returns Array(B, A)) + // select(Array(A, Array(B,A)), A) -> Array(B, A) + app_ref tmp_select_outer(arr.mk_select(const_outer.get(), a0.get()), m); + func_decl* select_outer_decl = tmp_select_outer->get_decl(); + te.add_production(select_outer_decl); + + // Enumerate terms of the nested array sort Array(A, Array(B, A)) + std::cout << "\nEnumerating terms of sort Array(A, Array(B, A)):\n"; + unsigned count = 0; + for (expr* e : te.enum_terms(array_A_arrayBA)) { + std::cout << " Term " << count << ": " << mk_pp(e, m) << "\n"; + count++; + if (count >= 15) break; // Limit output + } + + ENSURE(count >= 1); // At least the constant array + std::cout << "Enumerated " << count << " terms of sort Array(A, Array(B, A))\n"; + + te.display(std::cout); +} + +void tst_term_enumeration() { + tst_basic_enumeration(); + tst_enumeration_with_operators(); + tst_observational_equivalence_filter(); + tst_display(); + tst_bitvector_enumeration(); + tst_multiple_sorts(); + tst_nested_array_enumeration(); + std::cout << "All term_enumeration tests passed!\n"; +} diff --git a/src/test/tptp.cpp b/src/test/tptp.cpp index a96f31568a..c799f63477 100644 --- a/src/test/tptp.cpp +++ b/src/test/tptp.cpp @@ -111,6 +111,30 @@ R"(tff(c1,conjecture, $let([a: $int, b: $int], [a := 1, b := 2], $less($sum(a,b) "% 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"}, + // Parenthesized negation binds only the next literal, not the whole + // disjunction: "( ~ p | q )" is "(~p) | q", not "~(p | q)". With p + // asserted this is satisfiable (q true); the old whole-formula negation + // made it spuriously unsatisfiable. + {"fof-paren-negation-precedence", +R"(fof(a1,axiom, ( ~ p | q )). +fof(a2,axiom, p).)", + "% SZS status Satisfiable"}, + // A TPTP quantifier binds tighter than the binary connectives, so + // "! [X] : p(X) => g" is "(! [X] : p(X)) => g". With p(a), ~p(b), ~g the + // antecedent is false, so the implication holds (Theorem). The old parse + // "! [X] : (p(X) => g)" was false at X=a and reported CounterSatisfiable. + {"fof-quantifier-binds-tighter-than-implies", +R"(fof(a1,axiom, p(a)). +fof(a2,axiom, ~ p(b)). +fof(a3,axiom, ~ g). +fof(c1,conjecture, ! [X] : p(X) => g).)", + "% SZS status Theorem"}, + // Mixed Int/Real equality must use the arithmetic to_real coercion, not + // an uninterpreted boxing function: $to_int(3.0) = 3.0 is valid because + // to_real(3) = 3.0. Boxing severed the link and reported CounterSatisfiable. + {"tff-int-real-equality-coercion", +R"(tff(c1,conjecture, $to_int(3.0) = 3.0).)", "% SZS status Theorem"} }; for (auto const& c : cases) { @@ -123,4 +147,51 @@ R"(tff(c1,conjecture, $let(a: $int, a := 5, $let(b: $int, b := 3, $less(b,a)))). 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); + + // SZS status cross-checking against the annotated input status. + + // Matching annotation: no BUG flag. + { + std::string o = run_tptp( +R"(% Status : Unsatisfiable +cnf(c1,axiom, p(X)). +cnf(c2,axiom, ~ p(a)).)"); + ENSURE(o.find("% SZS status Unsatisfiable") != std::string::npos); + ENSURE(o.find("BUG") == std::string::npos); + } + + // Contradicting annotation (says Satisfiable, z3 finds Unsatisfiable): BUG. + { + std::string o = run_tptp( +R"(% SZS status Satisfiable +cnf(c1,axiom, p(X)). +cnf(c2,axiom, ~ p(a)).)"); + ENSURE(o.find("BUG") != std::string::npos); + ENSURE(o.find("expected Satisfiable") != std::string::npos); + } + + // Contradicting annotation (says Unsatisfiable, z3 finds Satisfiable): BUG. + { + std::string o = run_tptp( +R"(% Status : Unsatisfiable +fof(a1,axiom, p(a)).)"); + ENSURE(o.find("BUG") != std::string::npos); + } + + // Theorem annotation matches z3's Theorem verdict for conjectures: no BUG. + { + std::string o = run_tptp( +R"(% SZS status Theorem +fof(a1,axiom, ! [X] : (human(X) => mortal(X))). +fof(a2,axiom, human(socrates)). +fof(c1,conjecture, mortal(socrates)).)"); + ENSURE(o.find("% SZS status Theorem") != std::string::npos); + ENSURE(o.find("BUG") == std::string::npos); + } + + // Unannotated input: nothing to check, no BUG. + { + std::string o = run_tptp("fof(a1,axiom, p(a))."); + ENSURE(o.find("BUG") == std::string::npos); + } } diff --git a/src/util/dependency.h b/src/util/dependency.h index 61219a97ce..55d88291ac 100644 --- a/src/util/dependency.h +++ b/src/util/dependency.h @@ -47,8 +47,7 @@ public: value const& leaf_value() const { SASSERT(is_leaf()); return static_cast(this)->m_value; } }; - template - static void linearize_todo(ptr_vector& todo, vector& vs) { + static void linearize_todo(ptr_vector& todo, vector& vs) { unsigned qhead = 0; while (qhead < todo.size()) { dependency* d = todo[qhead]; @@ -70,8 +69,7 @@ public: d->unmark(); } - template - static void s_linearize(dependency* d, vector& vs) { + static void s_linearize(dependency* d, vector& vs) { if (!d) return; ptr_vector todo; @@ -225,8 +223,8 @@ public: } - template - void linearize(dependency * d, vector & vs) const { + + void linearize(dependency * d, vector & vs) const { if (!d) return; SASSERT(m_todo.empty()); @@ -236,8 +234,23 @@ public: m_todo.reset(); } - template - void linearize(ptr_vector& deps, vector & vs) const { + // Linearize the union of two dependencies without allocating a join node. + void linearize(dependency * d1, dependency * d2, vector & vs) const { + SASSERT(m_todo.empty()); + if (d1) { + d1->mark(); + m_todo.push_back(d1); + } + if (d2 && !d2->is_marked()) { + d2->mark(); + m_todo.push_back(d2); + } + if (!m_todo.empty()) + linearize_todo(m_todo, vs); + m_todo.reset(); + } + + void linearize(ptr_vector& deps, vector & vs) const { if (deps.empty()) return; SASSERT(m_todo.empty()); @@ -332,19 +345,20 @@ public: return m_dep_manager.contains(d, v); } - template - void linearize(dependency * d, vector & vs) const { + void linearize(dependency * d, vector & vs) const { return m_dep_manager.linearize(d, vs); - } + } - template - static vector const& s_linearize(dependency* d, vector& vs) { + void linearize(dependency * d1, dependency * d2, vector & vs) const { + return m_dep_manager.linearize(d1, d2, vs); + } + + static vector const& s_linearize(dependency* d, vector& vs) { dep_manager::s_linearize(d, vs); return vs; } - template - void linearize(ptr_vector& d, vector & vs) const { + void linearize(ptr_vector& d, vector & vs) const { return m_dep_manager.linearize(d, vs); } diff --git a/src/util/index_sort_with_mutations.h b/src/util/index_sort_with_mutations.h new file mode 100644 index 0000000000..9852a87f5c --- /dev/null +++ b/src/util/index_sort_with_mutations.h @@ -0,0 +1,79 @@ +/*++ +Copyright (c) 2026 Microsoft Corporation + +Module Name: + + index_sort_with_mutations.h + +Abstract: + + Bounds-safe, mutation-aware stable merge sort of an index permutation. + + This exists to be shared between sites whose ordering comparator is NOT a + fixed strict weak ordering over a single sort -- typically because the + comparator MUTATES the objects it compares (e.g. algebraic numbers, whose + comparison refines their isolating intervals) and may even throw when a + resource limit is hit mid-sort. Against such a comparator std::sort + (introsort) is undefined behavior: it relies on a comparator-derived + sentinel and re-compares a pivot repeatedly, so a comparison that + transiently strengthens from "uncertain" to "decided" invalidates the + sentinel and its *unguarded* insertion pass walks off the array -> an + out-of-bounds read -> SIGSEGV (a try/catch could not help). + + Merge sort is safe BECAUSE of how it meets a mutating comparator: + 1. It never re-compares a pair (each unordered pair is compared at exactly + one merge level), so "the verdict for this pair changed" cannot occur + within the sort. + 2. It uses no comparator-derived sentinel; every loop bound is arithmetic + (i < mid, j < hi), so an inconsistent comparator can only yield a wrong + order, never an out-of-bounds access or non-termination. + 3. If the comparator's refinement is monotone toward a true order, runs + are ordered by decided verdicts that later refinement cannot un-decide, + so each run stays sorted and deeper merges inherit cheaper comparisons. + It runs in O(n log n) comparisons and O(n) scratch, is stable, and unwinds + cleanly if the comparator throws on cancellation. + +Author: + + Lev Nachmanson 2026 + +--*/ +#pragma once + +#include + +/** + \brief Stable merge sort of an index permutation. + + \c perm and \c scratch must each point to an array of \c n unsigned values; + \c perm holds the permutation to sort (typically 0..n-1). \c less(a, b) takes + two element indices and returns true iff element \c a must come strictly + before element \c b. On return \c perm holds the sorted permutation and + \c scratch has been used as working space. Equal/undecided pairs keep their + relative order (stable). +*/ +template +void stable_index_merge_sort(unsigned* perm, unsigned* scratch, unsigned n, Less less) { + if (n < 2) + return; + unsigned* src = perm; + unsigned* dst = scratch; + for (unsigned width = 1; width < n; width <<= 1) { + for (unsigned lo = 0; lo < n; lo += (width << 1)) { + unsigned mid = std::min(lo + width, n); + unsigned hi = std::min(lo + (width << 1), n); + unsigned i = lo, j = mid, k = lo; + // Take from the right run only on a strict decrease, so equal/ + // undecided pairs keep their relative order (stable). + while (i < mid && j < hi) + dst[k++] = less(src[j], src[i]) ? src[j++] : src[i++]; + while (i < mid) + dst[k++] = src[i++]; + while (j < hi) + dst[k++] = src[j++]; + } + std::swap(src, dst); + } + if (src != perm) + std::copy(src, src + n, perm); +} diff --git a/src/util/mpz.cpp b/src/util/mpz.cpp index 10c345b0c9..7fd91404cb 100644 --- a/src/util/mpz.cpp +++ b/src/util/mpz.cpp @@ -700,7 +700,7 @@ mpz mpz_manager::mod2k(mpz const & a, unsigned k) { ensure_mpz_t a1(a); mk_big(result); MPZ_BEGIN_CRITICAL(); - mpz_tdiv_r_2exp(*result.m_ptr, a1(), k); + mpz_fdiv_r_2exp(*result.m_ptr, a1(), k); MPZ_END_CRITICAL(); #endif return result; @@ -1904,8 +1904,11 @@ std::string mpz_manager::to_string(mpz const & a) const { template unsigned mpz_manager::hash(mpz const & a) { - if (is_small(a)) - return ::abs(a.m_val); + if (is_small(a)) { + // compute abs in unsigned arithmetic: ::abs(INT_MIN) is undefined + unsigned u = static_cast(a.m_val); + return a.m_val < 0 ? 0u - u : u; + } #ifndef _MP_GMP unsigned sz = size(a); if (sz == 1) diff --git a/src/util/obj_hashtable.h b/src/util/obj_hashtable.h index 8ab689523f..58cbae50ab 100644 --- a/src/util/obj_hashtable.h +++ b/src/util/obj_hashtable.h @@ -62,10 +62,6 @@ public: struct key_data { Key * m_key = nullptr; Value m_value; - key_data() = default; - key_data(Key* k): m_key(k) {} - key_data(Key* k, Value const& v): m_key(k), m_value(v) {} - key_data(Key* k, Value&& v): m_key(k), m_value(std::move(v)) {} Value const & get_value() const { return m_value; } Key & get_key () const { return *m_key; } unsigned hash() const { return m_key->hash(); } @@ -137,15 +133,15 @@ public: } void insert(Key * const k, Value const & v) { - m_table.insert(key_data(k, v)); + m_table.insert(key_data{k, v}); } void insert(Key * const k, Value && v) { - m_table.insert(key_data(k, std::move(v))); + m_table.insert(key_data{k, std::move(v)}); } Value& insert_if_not_there(Key * k, Value const & v) { - return m_table.insert_if_not_there2(key_data(k, v))->get_data().m_value; + return m_table.insert_if_not_there2(key_data{k, v})->get_data().m_value; } Value& insert_if_not_there(Key * k, Value && v) { @@ -195,7 +191,7 @@ public: } iterator find_iterator(Key * k) const { - return m_table.find(key_data(k)); + return m_table.find(key_data{k}); } bool contains(Key * k) const { @@ -203,7 +199,7 @@ public: } void remove(Key * k) { - m_table.remove(key_data(k)); + m_table.remove(key_data{k}); } void erase(Key * k) { @@ -214,7 +210,7 @@ public: void get_collisions(Key * k, vector& collisions) { vector cs; - m_table.get_collisions(key_data(k), cs); + m_table.get_collisions(key_data{k}, cs); for (key_data const& kd : cs) { collisions.push_back(kd.m_key); } diff --git a/src/util/obj_pair_hashtable.h b/src/util/obj_pair_hashtable.h index e9d2e9bb54..78355b9bb7 100644 --- a/src/util/obj_pair_hashtable.h +++ b/src/util/obj_pair_hashtable.h @@ -59,17 +59,13 @@ protected: class entry; public: class key_data { - Key1 * m_key1; - Key2 * m_key2; + Key1 * m_key1 = nullptr; + Key2 * m_key2 = nullptr; Value m_value; - unsigned m_hash; + unsigned m_hash = 0; friend class entry; public: - key_data(): - m_key1(nullptr), - m_key2(nullptr), - m_hash(0) { - } + key_data() = default; key_data(Key1 * k1, Key2 * k2): m_key1(k1), m_key2(k2) { diff --git a/src/util/obj_triple_hashtable.h b/src/util/obj_triple_hashtable.h index 7d75084fe9..00a7a2845d 100644 --- a/src/util/obj_triple_hashtable.h +++ b/src/util/obj_triple_hashtable.h @@ -60,19 +60,14 @@ protected: class entry; public: class key_data { - Key1 * m_key1; - Key2 * m_key2; - Key3 * m_key3; + Key1 * m_key1 = nullptr; + Key2 * m_key2 = nullptr; + Key3 * m_key3 = nullptr; Value m_value; - unsigned m_hash; + unsigned m_hash = 0; friend class entry; public: - key_data(): - m_key1(nullptr), - m_key2(nullptr), - m_key3(nullptr), - m_hash(0) { - } + key_data() = default; key_data(Key1 * k1, Key2 * k2, Key3 * k3): m_key1(k1), m_key2(k2), diff --git a/src/util/search_tree.h b/src/util/search_tree.h index 85294d550c..9b9f15064b 100644 --- a/src/util/search_tree.h +++ b/src/util/search_tree.h @@ -50,7 +50,6 @@ namespace search_tree { 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) {} @@ -68,9 +67,17 @@ namespace search_tree { literal const &get_literal() const { return m_literal; } + bool path_contains_atom(literal const& l) const { + for (node const* n = this; n; n = n->parent()) + if (!Config::literal_is_null(n->get_literal()) && Config::same_atom(n->get_literal(), l)) + return true; + return false; + } void split(literal const &a, literal const &b) { SASSERT(!Config::literal_is_null(a)); SASSERT(!Config::literal_is_null(b)); + VERIFY(!path_contains_atom(a)); + VERIFY(!path_contains_atom(b)); if (m_status != status::active) return; SASSERT(!m_left); @@ -148,12 +155,6 @@ namespace search_tree { 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 { @@ -340,7 +341,6 @@ 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); @@ -444,8 +444,8 @@ namespace search_tree { // 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)) + bool try_split(node *n, literal const &a, literal const &b, unsigned effort) { + if (is_lease_canceled(n)) return false; // Record at most one effort contribution per concurrent round on this node. @@ -544,8 +544,8 @@ namespace search_tree { 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; + bool is_lease_canceled(node* n) const { + return !n || n->get_status() == status::closed; } vector const &get_core_from_root() const { diff --git a/src/util/symbol_table.h b/src/util/symbol_table.h index 5d2cce7268..c6dbb9f827 100644 --- a/src/util/symbol_table.h +++ b/src/util/symbol_table.h @@ -30,17 +30,6 @@ class symbol_table { struct key_data { symbol m_key; T m_data; - - key_data() = default; - - explicit key_data(symbol k): - m_key(k) { - } - - key_data(symbol k, const T & d): - m_key(k), - m_data(d) { - } }; struct key_data_hash_proc { @@ -129,7 +118,7 @@ public: } bool contains(symbol key) const { - return m_sym_table.contains(key_data(key)); + return m_sym_table.contains(key_data{key}); } unsigned get_scope_level() const { @@ -148,11 +137,11 @@ public: m_trail_stack.push_back(dummy); key_data & new_entry = m_trail_stack.back(); new_entry.m_key = symbol::mark(new_entry.m_key); - m_sym_table.insert(key_data(key, data)); + m_sym_table.insert(key_data{key, data}); } } else { - m_sym_table.insert(key_data(key, data)); + m_sym_table.insert(key_data{key, data}); } }