3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-08-08 06:52:26 +00:00

Merge remote-tracking branch 'origin/master' into seq-dnf-opt

# Conflicts:
#	src/ast/rewriter/seq_monadic.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 57b9b87e-950a-49ea-bbb3-ed585646a5a9
This commit is contained in:
Nikolaj Bjorner 2026-08-05 17:24:24 -07:00
commit 8b92513413
8 changed files with 361 additions and 108 deletions

File diff suppressed because one or more lines are too long

View file

@ -29,6 +29,7 @@ safe-outputs:
network: defaults
tools:
github:
mode: gh-proxy
toolsets: [default, actions]
bash: [":*"]
timeout-minutes: 90
@ -44,7 +45,7 @@ steps:
# Clang-Tidy Warning Fixer
You are an AI agent that uses clang-tidy warning output captured in GitHub Actions logs, proposes conservative fixes, and creates a GitHub issue with ready-to-apply git diffs.
You are an AI agent that uses clang-tidy warning output captured in a GitHub Actions artifact, proposes conservative fixes, and creates a GitHub issue with ready-to-apply git diffs.
## Current Context
@ -63,58 +64,36 @@ This workflow is only for `Z3Prover/z3`.
If `${{ github.repository }}` is not `Z3Prover/z3`, call `noop` immediately with a short explanation.
### 1. Retrieve logs from `clang-tidy-warning-report.yml`
### 1. Retrieve the artifact from `clang-tidy-warning-report.yml`
Use GitHub MCP tools (not `gh`) to retrieve job logs from the triggering run. Do not use `download_workflow_run_artifact`: it may be unavailable, and this workflow must operate entirely from Actions logs.
Use the authenticated `gh` CLI proxy to retrieve the warning artifact from the triggering run.
1. Determine source run ID:
- If `${{ github.event.workflow_run.id }}` is present, use it.
- For manual dispatch, call `github-mcp-server-actions_list` (`list_workflow_runs`) for workflow `clang-tidy-warning-report.yml` and select the latest `completed` run.
2. List jobs for that run with `github-mcp-server-actions_list` (`list_workflow_jobs`).
3. Identify the job named `Build Z3 with clang-tidy warnings`.
4. Retrieve its logs with `github-mcp-server-get_job_logs` using `return_content: true` and a large `tail_lines` value so the appended summary block is included.
5. Save the returned log content locally for repeatable analysis:
- For manual dispatch, use `gh run list` for workflow `clang-tidy-warning-report.yml` and select the latest completed run.
2. Download and extract the artifact:
```bash
RUN_ID="${{ github.event.workflow_run.id }}"
if [ -z "$RUN_ID" ]; then
RUN_ID="$(gh run list --repo "${{ github.repository }}" \
--workflow clang-tidy-warning-report.yml --status completed --limit 1 \
--json databaseId --jq '.[0].databaseId')"
fi
rm -rf /tmp/gh-aw/clang-tidy-warning-report
mkdir -p /tmp/gh-aw/clang-tidy-warning-report
cat <<'EOF' > /tmp/gh-aw/clang-tidy-warning-report/build.log
$JOB_LOG_CONTENT
EOF
cp /tmp/gh-aw/clang-tidy-warning-report/build.log /tmp/gh-aw/clang-tidy-warning-report/combined.log
gh run download "$RUN_ID" --repo "${{ github.repository }}" \
--name "clang-tidy-warning-report-$RUN_ID" \
--dir /tmp/gh-aw/clang-tidy-warning-report
ls -la /tmp/gh-aw/clang-tidy-warning-report
```
The source workflow emits a marker-delimited summary near the end of the job log:
- `CLANG_TIDY_WARNING_REPORT_BEGIN`
- `CLANG_TIDY_STATUS_BEGIN` / `CLANG_TIDY_STATUS_END`
- `CLANG_TIDY_WARNINGS_BEGIN` / `CLANG_TIDY_WARNINGS_END`
- `CLANG_TIDY_WARNING_REPORT_END`
Extract the summary into local files:
```bash
sed -n '/^CLANG_TIDY_STATUS_BEGIN$/,/^CLANG_TIDY_STATUS_END$/p' \
/tmp/gh-aw/clang-tidy-warning-report/combined.log | sed '1d;$d' \
> /tmp/gh-aw/clang-tidy-warning-report/status.txt
sed -n '/^CLANG_TIDY_WARNINGS_BEGIN$/,/^CLANG_TIDY_WARNINGS_END$/p' \
/tmp/gh-aw/clang-tidy-warning-report/combined.log | sed '1d;$d' \
> /tmp/gh-aw/clang-tidy-warning-report/warnings.txt
```
If the marker block is missing, fall back to grepping the full log:
```bash
grep -nE 'warning:|error:|clang-tidy' /tmp/gh-aw/clang-tidy-warning-report/combined.log \
> /tmp/gh-aw/clang-tidy-warning-report/warnings.txt || true
```
Expect at minimum `build.log`, `combined.log`, and `warnings.txt`. Prefer using `status.txt` when extracted successfully. If the job log is unavailable or empty, call `noop` with a concise explanation.
Expect `configure.log`, `build.log`, `combined.log`, `warnings.txt`, and `status.txt`. If the artifact is unavailable, expired, or empty, call `noop` with a concise explanation.
### 2. Extract actionable diagnostics
Analyze log-derived files from this run:
Analyze artifact files from this run:
- `/tmp/gh-aw/clang-tidy-warning-report/warnings.txt`
- `/tmp/gh-aw/clang-tidy-warning-report/build.log`
- `/tmp/gh-aw/clang-tidy-warning-report/combined.log`
@ -196,7 +175,7 @@ Issue content must include:
- proposed fixes as unified diffs (full diff text, not prose only)
- short assignment-ready checklist for Copilot (one checkbox per proposed fix)
If no actionable warnings are found, or the source job logs are missing/corrupt, call `noop` with a concise explanation.
If no actionable warnings are found, or the source artifact is missing/corrupt, call `noop` with a concise explanation.
## Guidelines
@ -204,4 +183,4 @@ If no actionable warnings are found, or the source job logs are missing/corrupt,
- Prefer no issue over risky or speculative patch suggestions.
- Keep fixes surgical and easy to review.
- Focus only on diagnostics produced by the referenced `clang-tidy-warning-report.yml` run.
- Prefer workflow job logs over cross-run artifact downloads, even if artifact metadata is visible.
- Use only the warning artifact from the selected workflow run.

View file

@ -59,6 +59,36 @@ Author:
#include <algorithm>
#include <unordered_set>
namespace {
char const* mode_name(seq::transition_mode mode) {
switch (mode) {
case seq::transition_mode::brzozowski_tm:
return "brzozowski";
case seq::transition_mode::light_antimirov_tm:
return "light-antimirov";
default:
return "unknown";
}
}
char const* bail_name(unsigned i) {
static char const* const names[] = {
"unsupported",
"state-cap",
"dnf-cap",
"budget",
"resource",
"nullability",
"guard"
};
return i < std::size(names) ? names[i] : "unknown";
}
char const* result_name(lbool r) {
return r == l_true ? "sat" : r == l_false ? "unsat" : "unknown";
}
}
expr_ref seq_monadic::der_elem(expr* r, expr* elem) {
expr* cached = nullptr;
@ -375,6 +405,8 @@ unsigned seq_monadic::var_index(expr* v) {
}
void seq_monadic::reset_search() {
m_seq_sort = nullptr;
m_elem_sort = nullptr;
m_atoms.reset();
m_regexes.reset();
m_vars.reset();
@ -799,39 +831,43 @@ lbool seq_monadic::choose_cont(unsigned vi, svector<unsigned> const& S, unsigned
}
lbool seq_monadic::decide(membership_vec const& memberships) {
m_last_search_memberships = memberships;
m_model.reset();
if (memberships.empty())
return l_true; // empty conjunction is vacuously true
reset_search(); // clear the caches before dropping the
m_pin.reset(); // pins that keep their keys alive
m_rp_cache.maybe_reset(1u << 16);
m_rw.get_derive().maybe_reset_cached_cofactors(1u << 16);
m_budget = 200000;
m_giveup = false;
if (!prepare(memberships))
return l_undef;
lbool r;
if (m_config.m_state_search) {
// Build one cursor per membership at its regex start; initial_normalize consumes
// leading constants so every active cursor exposes a variable head.
m_cursors.reset();
for (unsigned mi = 0; mi < m_atoms.size(); ++mi)
m_cursors.push_back(cursor{ 0, m_regexes.get(mi), false });
m_last_var = UINT_MAX;
lbool norm = initial_normalize();
r = (norm == l_false) ? l_false : search();
lbool r = l_true; // empty conjunction is vacuously true
if (!memberships.empty() && !prepare(memberships))
r = l_undef;
else if (!memberships.empty()) {
if (m_config.m_state_search) {
// Build one cursor per membership at its regex start; initial_normalize consumes
// leading constants so every active cursor exposes a variable head.
m_cursors.reset();
for (unsigned mi = 0; mi < m_atoms.size(); ++mi)
m_cursors.push_back(cursor{ 0, m_regexes.get(mi), false });
m_last_var = UINT_MAX;
lbool norm = initial_normalize();
r = (norm == l_false) ? l_false : search();
}
else
r = dfs_membership(0);
}
else
r = dfs_membership(0);
if (r != l_true)
m_model.reset();
m_last_search_result = r;
return r;
}
lbool seq_monadic::solve(expr* term, expr* R) {
m_core.reset();
membership_vec mv;
mv.push_back({ expr_ref(term, m), expr_ref(R, m), nullptr });
return decide(mv);
m_last_result = decide(mv);
return m_last_result;
}
void seq_monadic::add(expr* term, expr* regex, void* d) {
@ -930,9 +966,132 @@ void seq_monadic::minimize_core(membership_vec const& memberships) {
lbool seq_monadic::check() {
m_core.reset();
lbool r = decide(m_memberships);
if (r == l_false)
if (r == l_false) {
minimize_core(m_memberships);
return r;
m_model.reset();
}
m_last_result = r;
return m_last_result;
}
std::ostream& seq_monadic::display(std::ostream& out) const {
auto display_expr = [&](expr* e) {
if (e)
out << mk_pp(e, m);
else
out << "null";
};
out << "(seq-monadic\n"
<< " :mode " << mode_name(m_config.m_mode) << "\n"
<< " :generate-model " << (m_config.m_model ? "true" : "false") << "\n"
<< " :minimize-core " << (m_config.m_min_core ? "true" : "false") << "\n"
<< " :last-result " << result_name(m_last_result) << "\n"
<< " :budget " << m_budget << "\n"
<< " :giveup " << (m_giveup ? "true" : "false") << "\n"
<< " :sequence-sort ";
if (m_seq_sort)
out << mk_pp(m_seq_sort, m);
else
out << "null";
out << "\n :element-sort ";
if (m_elem_sort)
out << mk_pp(m_elem_sort, m);
else
out << "null";
out << "\n :memberships (";
for (unsigned i = 0; i < m_memberships.size(); ++i) {
auto const& [term, regex, dep] = m_memberships[i];
out << "\n [" << i << "] ";
display_expr(term);
out << " in ";
display_expr(regex);
out << " :dependency " << dep;
}
if (!m_memberships.empty())
out << "\n ";
out << ")\n :model (";
for (auto const& [var, value] : m_model) {
out << "\n ";
display_expr(var);
out << " -> ";
display_expr(value);
}
if (!m_model.empty())
out << "\n ";
out << ")\n :core (";
for (void* dep : m_core)
out << " " << dep;
out << " )";
out << "\n :last-internal-search\n"
<< " (:result " << result_name(m_last_search_result)
<< "\n :memberships (";
for (unsigned i = 0; i < m_last_search_memberships.size(); ++i) {
auto const& [term, regex, dep] = m_last_search_memberships[i];
out << "\n [" << i << "] ";
display_expr(term);
out << " in ";
display_expr(regex);
out << " :dependency " << dep;
}
if (!m_last_search_memberships.empty())
out << "\n ";
out << ")\n :variables (";
for (expr* var : m_vars) {
out << " ";
display_expr(var);
}
out << " )\n :parsed-memberships (";
for (unsigned mi = 0; mi < m_atoms.size(); ++mi) {
out << "\n [" << mi << "] :regex ";
display_expr(m_regexes.get(mi));
out << " :atoms (";
for (atom const& a : m_atoms[mi]) {
out << " " << (a.is_var ? "var:" : "elem:");
display_expr(a.is_var ? a.var.get() : a.elem.get());
}
out << " )";
}
if (!m_atoms.empty())
out << "\n ";
out << ")\n :groups (";
for (unsigned vi = 0; vi < m_groups.size(); ++vi) {
out << "\n ";
display_expr(m_vars[vi]);
out << " (";
for (component const& c : m_groups[vi]) {
out << "\n ";
display_expr(c.state);
if (c.target) {
out << " -> ";
display_expr(c.target);
}
else {
out << " nullable";
}
}
if (!m_groups[vi].empty())
out << "\n ";
out << ")";
}
if (!m_groups.empty())
out << "\n ";
out << ")\n"
<< " :undefined-variables " << m_undef_vars << "\n"
<< " :group-cache-size " << m_group_cache.size() << "\n"
<< " :derivative-cache-size " << m_der_cache.size() << "\n"
<< " :nullable-cache-size " << m_nullable_cache.size() << "\n"
<< " :live-cache-size " << m_live_cache.size() << "\n"
<< " :pinned-expressions " << m_pin.size() << ")\n";
out << " :statistics\n"
<< " (:cofactor-calls " << m_stats.m_cofactor_calls << "\n"
<< " :states " << m_stats.m_states;
for (unsigned i = 0; i < static_cast<unsigned>(bail_reason::num_reasons); ++i)
out << "\n :bail-" << bail_name(i) << " " << m_stats.m_bails[i];
return out << "))\n";
}
void seq_monadic::collect_statistics(::statistics& st) const {

View file

@ -121,8 +121,11 @@ class seq_monadic {
// seq_rewriter's own cache is capped and flushed whole
using membership_vec = vector<std::tuple<expr_ref, expr_ref, void*>>;
membership_vec m_memberships; // asserted (term in regex, dep) for check()
membership_vec m_last_search_memberships; // inputs used by the last internal decide()
ptr_vector<void> m_core; // dependencies of an unsat subset, filled by check() on l_false
std::function<bool(expr *)> m_is_var; // predicate for whether a term is a sequence variable
lbool m_last_result = l_undef; // result of the last public solve()/check()
lbool m_last_search_result = l_undef; // result of the last internal decide()
seq_util& u() const { return m_rw.u(); }
seq_util::rex& re() const { return m_rw.u().re; }
@ -295,6 +298,9 @@ public:
void collect_statistics(::statistics &st) const;
// Display asserted constraints, result artifacts, search state, caches, and counters.
std::ostream& display(std::ostream& out) const;
seq::transition_mode mode() const { return m_config.m_mode; }
// Enable/disable model generation (default: enabled). When enabled, a successful

View file

@ -373,6 +373,19 @@ bool cmd_context::contains_func_decl(symbol const& s, unsigned n, sort* const* d
return m_func_decls.find(s, fs) && fs.contains(n, domain, range);
}
bool cmd_context::builtin_signature_collides(symbol const& s, unsigned arity, sort* const* domain) const {
expr_ref_vector args(m());
for (unsigned i = 0; i < arity; ++i)
args.push_back(m().mk_var(i, domain[i]));
expr_ref result(m());
try {
return try_mk_builtin_app(s, arity, args.data(), 0, nullptr, nullptr, result);
}
catch (ast_exception&) {
return false;
}
}
bool cmd_context::contains_macro(symbol const& s) const {
macro_decls decls;
return m_macros.find(s, decls) && !decls.empty();
@ -938,11 +951,12 @@ void cmd_context::insert(symbol const & s, func_decl * f) {
if (contains_macro(s, f)) {
throw cmd_exception("invalid declaration, named expression already defined with this name ", s);
}
#if 0
if (m_builtin_decls.contains(s)) {
throw cmd_exception("invalid declaration, builtin symbol ", s);
if (builtin_signature_collides(s, f->get_arity(), f->get_domain())) {
std::string msg = "invalid declaration, builtin symbol '";
msg += s.str();
msg += "' has the same argument sorts";
throw cmd_exception(std::move(msg));
}
#endif
func_decls & fs = m_func_decls.insert_if_not_there(s, func_decls());
if (!fs.insert(m(), f)) {
if (m_allow_duplicate_declarations)
@ -980,11 +994,12 @@ void cmd_context::insert(symbol const & s, psort_decl * p) {
void cmd_context::insert(symbol const & s, unsigned arity, sort *const* domain, expr * t) {
expr_ref _t(t, m());
#if 0
if (m_builtin_decls.contains(s)) {
throw cmd_exception("invalid macro/named expression, builtin symbol ", s);
if (builtin_signature_collides(s, arity, domain)) {
std::string msg = "invalid named expression, builtin symbol '";
msg += s.str();
msg += "' has the same argument sorts";
throw cmd_exception(std::move(msg));
}
#endif
if (contains_macro(s, arity, domain)) {
throw cmd_exception("named expression already defined");
}
@ -2570,4 +2585,3 @@ std::ostream & operator<<(std::ostream & out, cmd_context::status st) {
}
return out;
}

View file

@ -364,6 +364,7 @@ protected:
void mk_solver();
bool contains_func_decl(symbol const& s, unsigned n, sort* const* domain, sort* range) const;
bool builtin_signature_collides(symbol const& s, unsigned arity, sort* const* domain) const;
bool contains_macro(symbol const& s) const;
bool contains_macro(symbol const& s, func_decl* f) const;
@ -581,4 +582,3 @@ public:
std::ostream & operator<<(std::ostream & out, cmd_context::status st);

View file

@ -27,6 +27,7 @@ Author:
#include "params/smt_params.h"
#include "smt/smt_kernel.h"
#include <iostream>
#include <sstream>
#include <set>
namespace {
@ -467,6 +468,68 @@ public:
std::cout << (trail_ok ? " OK " : " FAIL ")
<< "check preserves assertions and pop removes them\n";
std::cout << "=== seq_monadic: display ===\n";
m_trail.push_scope();
unsigned display_dep = 0;
m_mon.set_gen_model(true);
m_mon.add(x, aaS, &display_dep);
lbool display_result = m_mon.check();
std::ostringstream display_out;
m_mon.display(display_out);
std::string display_text = display_out.str();
bool display_ok =
display_result == l_true &&
display_text.find("(seq-monadic") != std::string::npos &&
display_text.find(":memberships") != std::string::npos &&
display_text.find(":model") != std::string::npos &&
display_text.find(":last-result sat") != std::string::npos &&
display_text.find(":last-internal-search") != std::string::npos &&
display_text.find(":parsed-memberships") != std::string::npos &&
display_text.find(":statistics") != std::string::npos &&
display_text.find("x") != std::string::npos;
m_trail.pop_scope(1);
if (!display_ok) ++m_fail;
std::cout << (display_ok ? " OK " : " FAIL ")
<< "display exposes readable solver state\n";
m_trail.push_scope();
unsigned display_dep1 = 1, display_dep2 = 2;
m_mon.set_min_core(true);
m_mon.add(x, aaS, &display_dep1);
m_mon.add(x, a_aaS, &display_dep2);
lbool unsat_display_result = m_mon.check();
std::ostringstream unsat_display_out;
m_mon.display(unsat_display_out);
std::string unsat_display_text = unsat_display_out.str();
bool unsat_display_ok =
unsat_display_result == l_false &&
unsat_display_text.find(":last-result unsat") != std::string::npos &&
unsat_display_text.find(":model ()") != std::string::npos &&
unsat_display_text.find(":last-internal-search") != std::string::npos;
m_trail.pop_scope(1);
m_mon.set_min_core(false);
if (!unsat_display_ok) ++m_fail;
std::cout << (unsat_display_ok ? " OK " : " FAIL ")
<< "display distinguishes unsat result from core-search state\n";
lbool solve_display_result = m_mon.solve(x, aaS);
std::ostringstream solve_display_out;
m_mon.display(solve_display_out);
bool solve_display_ok =
solve_display_result == l_true &&
solve_display_out.str().find(":core ( )") != std::string::npos;
lbool empty_display_result = m_mon.check();
std::ostringstream empty_display_out;
m_mon.display(empty_display_out);
std::string empty_display_text = empty_display_out.str();
bool empty_display_ok =
empty_display_result == l_true &&
empty_display_text.find(":sequence-sort null") != std::string::npos &&
empty_display_text.find(":element-sort null") != std::string::npos;
if (!solve_display_ok || !empty_display_ok) ++m_fail;
std::cout << (solve_display_ok && empty_display_ok ? " OK " : " FAIL ")
<< "display clears artifacts across solve and empty check\n";
std::cout << "=== seq_monadic: length bounds ===\n";
auto check_bound = [&](char const* name, expr* regex, unsigned bound, bool is_lo,
lbool expected) {

View file

@ -257,6 +257,36 @@ void test_symbol_escape() {
std::cout << "done evaluating\n";
}
void test_builtin_signature_clash() {
char const* rejected[] = {
"(declare-fun and (Bool Bool) Int)",
"(define-fun not ((a Bool)) Bool false)",
"(declare-const true Bool)",
"(define-fun = ((a Int) (b Int)) Bool true)",
"(define-fun ite ((c Bool) (a Int) (b Int)) Int 0)",
"(define-fun + ((a Int) (b Int)) Int 0)",
"(define-fun-rec and ((a Bool) (b Bool)) Bool false)"
};
for (char const* spec : rejected) {
Z3_context ctx = Z3_mk_context(nullptr);
Z3_set_error_handler(ctx, setError);
is_error = false;
Z3_parse_smtlib2_string(ctx, spec, 0, nullptr, nullptr, 0, nullptr, nullptr);
ENSURE(is_error);
Z3_del_context(ctx);
}
Z3_context ctx = Z3_mk_context(nullptr);
Z3_set_error_handler(ctx, setError);
test_eval(ctx,
"(declare-fun and (Int Int) Int)\n"
"(assert (= (and 1 2) 0))\n"
"(check-sat)\n",
false);
Z3_del_context(ctx);
}
void tst_smt2print_parse() {
// test basic datatypes
@ -326,6 +356,7 @@ void tst_smt2print_parse() {
test_ho_choice_expression();
test_symbol_escape();
test_builtin_signature_clash();
// Regression test for GitHub issue #10166:
// With (set-option :smtlib2_compliant true), a formula involving to_real