3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-04 06:16:09 +00:00

Merge branch 'master' of https://github.com/z3prover/z3 into derive

This commit is contained in:
Nikolaj Bjorner 2026-06-09 15:18:29 -07:00
commit f02391a01f
135 changed files with 4918 additions and 2301 deletions

View file

@ -242,21 +242,14 @@ func_decl_info::func_decl_info(family_id family_id, decl_kind k, unsigned num_pa
m_injective(false),
m_idempotent(false),
m_skolem(false),
m_lambda(false),
m_polymorphic(false) {
}
bool func_decl_info::operator==(func_decl_info const & info) const {
return decl_info::operator==(info) &&
m_left_assoc == info.m_left_assoc &&
m_right_assoc == info.m_right_assoc &&
m_flat_associative == info.m_flat_associative &&
m_commutative == info.m_commutative &&
m_chainable == info.m_chainable &&
m_pairwise == info.m_pairwise &&
m_injective == info.m_injective &&
m_skolem == info.m_skolem &&
m_lambda == info.m_lambda;
return decl_info::operator==(info) && m_left_assoc == info.m_left_assoc && m_right_assoc == info.m_right_assoc &&
m_flat_associative == info.m_flat_associative && m_commutative == info.m_commutative &&
m_chainable == info.m_chainable && m_pairwise == info.m_pairwise && m_injective == info.m_injective &&
m_skolem == info.m_skolem;
}
std::ostream & operator<<(std::ostream & out, func_decl_info const & info) {
@ -270,7 +263,6 @@ std::ostream & operator<<(std::ostream & out, func_decl_info const & info) {
if (info.is_injective()) out << " :injective ";
if (info.is_idempotent()) out << " :idempotent ";
if (info.is_skolem()) out << " :skolem ";
if (info.is_lambda()) out << " :lambda ";
if (info.is_polymorphic()) out << " :polymorphic ";
return out;
}
@ -1625,19 +1617,6 @@ bool ast_manager::are_distinct(expr* a, expr* b) const {
return false;
}
void ast_manager::add_lambda_def(func_decl* f, quantifier* q) {
TRACE(model, tout << "add lambda def " << mk_pp(q, *this) << "\n");
m_lambda_defs.insert(f, q);
f->get_info()->set_lambda(true);
inc_ref(q);
}
quantifier* ast_manager::is_lambda_def(func_decl* f) {
if (f->get_info() && f->get_info()->is_lambda())
return m_lambda_defs[f];
return nullptr;
}
void ast_manager::register_plugin(family_id id, decl_plugin * plugin) {
SASSERT(m_plugins.get(id, 0) == 0);
@ -1832,10 +1811,6 @@ void ast_manager::delete_node(ast * n) {
m_poly_roots.erase(f);
if (f->m_info != nullptr) {
func_decl_info * info = f->get_info();
if (info->is_lambda()) {
push_dec_ref(m_lambda_defs[f]);
m_lambda_defs.remove(f);
}
info->del_eh(*this);
dealloc(info);
}

View file

@ -404,7 +404,6 @@ struct func_decl_info : public decl_info {
bool m_injective:1;
bool m_idempotent:1;
bool m_skolem:1;
bool m_lambda:1;
bool m_polymorphic:1;
func_decl_info(family_id family_id = null_family_id, decl_kind k = null_decl_kind, unsigned num_parameters = 0, parameter const * parameters = nullptr);
@ -419,7 +418,6 @@ struct func_decl_info : public decl_info {
bool is_injective() const { return m_injective; }
bool is_idempotent() const { return m_idempotent; }
bool is_skolem() const { return m_skolem; }
bool is_lambda() const { return m_lambda; }
bool is_polymorphic() const { return m_polymorphic; }
void set_associative(bool flag = true) { m_left_assoc = flag; m_right_assoc = flag; }
@ -432,7 +430,6 @@ struct func_decl_info : public decl_info {
void set_injective(bool flag = true) { m_injective = flag; }
void set_idempotent(bool flag = true) { m_idempotent = flag; }
void set_skolem(bool flag = true) { m_skolem = flag; }
void set_lambda(bool flag = true) { m_lambda = flag; }
void set_polymorphic(bool flag = true) { m_polymorphic = flag; }
bool operator==(func_decl_info const & info) const;
@ -661,7 +658,6 @@ public:
bool is_pairwise() const { return get_info() != nullptr && get_info()->is_pairwise(); }
bool is_injective() const { return get_info() != nullptr && get_info()->is_injective(); }
bool is_skolem() const { return get_info() != nullptr && get_info()->is_skolem(); }
bool is_lambda() const { return get_info() != nullptr && get_info()->is_lambda(); }
bool is_idempotent() const { return get_info() != nullptr && get_info()->is_idempotent(); }
bool is_polymorphic() const { return get_info() != nullptr && get_info()->is_polymorphic(); }
unsigned get_arity() const { return m_arity; }
@ -1513,7 +1509,6 @@ protected:
proof_gen_mode m_proof_mode;
bool m_int_real_coercions; // If true, use hack that automatically introduces to_int/to_real when needed.
ast_table m_ast_table;
obj_map<func_decl, quantifier*> m_lambda_defs;
id_gen m_expr_id_gen;
id_gen m_decl_id_gen;
sort * m_bool_sort;
@ -1643,15 +1638,7 @@ public:
bool are_distinct(expr * a, expr * b) const;
bool contains(ast * a) const { return m_ast_table.contains(a); }
bool is_lambda_q(quantifier* q) const { return q->get_qid() == m_lambda_def; }
void add_lambda_def(func_decl* f, quantifier* q);
quantifier* is_lambda_def(func_decl* f);
quantifier* is_lambda_def(expr* e) { return is_app(e) ? is_lambda_def(to_app(e)->get_decl()) : nullptr; }
obj_map<func_decl, quantifier*> const& lambda_defs() const { return m_lambda_defs; }
symbol const& lambda_def_qid() const { return m_lambda_def; }
unsigned get_num_asts() const { return m_ast_table.size(); }
void debug_ref_count() { m_debug_ref_count = true; }

View file

@ -21,6 +21,7 @@ Revision History:
#include "ast/for_each_ast.h"
#include "ast/arith_decl_plugin.h"
#include "ast/datatype_decl_plugin.h"
#include "ast/ast_smt2_pp.h"
// #define AST_LL_PP_SHOW_FAMILY_NAME
@ -44,7 +45,7 @@ class ll_printer {
}
void display_name(func_decl * decl) {
m_out << decl->get_name();
m_out << ensure_quote(decl->get_name());
}
bool process_numeral(expr * n) {

View file

@ -507,6 +507,7 @@ class smt_printer {
case forall_k: m_out << "forall "; break;
case exists_k: m_out << "exists "; break;
case lambda_k: m_out << "lambda "; break;
case choice_k: m_out << "choice "; break;
}
m_out << "(";
for (unsigned i = 0; i < q->get_num_decls(); ++i) {

View file

@ -181,20 +181,12 @@ void ast_translation::mk_func_decl(func_decl * f, frame & fr) {
new_fi.set_injective(fi->is_injective());
new_fi.set_skolem(fi->is_skolem());
new_fi.set_idempotent(fi->is_idempotent());
new_fi.set_lambda(fi->is_lambda());
new_f = m_to_manager.mk_func_decl(f->get_name(),
f->get_arity(),
new_domain,
new_range,
new_fi);
if (new_fi.is_lambda()) {
quantifier* q = from().is_lambda_def(f);
ast_translation tr(from(), to());
quantifier* new_q = tr(q);
to().add_lambda_def(new_f, new_q);
}
}
TRACE(ast_translation,
tout << f->get_name() << " "; if (fi) tout << *fi; tout << "\n";

View file

@ -1951,14 +1951,12 @@ namespace euf {
enode * get_next_f_app(func_decl * lbl, unsigned num_expected_args, enode * first, enode * curr) {
curr = curr->get_next();
enode *matching_cgr = nullptr, *min_gen_match = nullptr;
while (curr != first) {
get_f_app(lbl, num_expected_args, curr, matching_cgr, min_gen_match);
if (curr->get_decl() == lbl && curr->num_args() == num_expected_args && curr->is_cgr())
return curr;
curr = curr->get_next();
}
if (matching_cgr)
update_max_generation(min_gen_match, first);
return matching_cgr;
return nullptr;
}
/**

View file

@ -240,7 +240,6 @@ namespace euf {
else
break;
}
r = unfold_lambda_def(r);
return r;
}
@ -254,34 +253,6 @@ namespace euf {
}
}
// We assume that m_rewriter should produce
// something amounting to weak-head normal form WHNF
// Unfold a lambda-def application f(args) to the corresponding lambda expression.
// For a func_decl f with arity n and lambda-def quantifier (lambda (x1..xk) body),
// f(a1,...,an) is unfolded to (lambda (x1..xk) body[params := a1..an]).
// For a constant f (arity 0) that is a lambda-def, returns the lambda directly.
expr_ref ho_matcher::unfold_lambda_def(expr* e) const {
if (!is_app(e))
return expr_ref(e, m);
app* a = to_app(e);
func_decl* f = a->get_decl();
quantifier* lam = m.is_lambda_def(f);
if (!lam)
return expr_ref(e, m);
unsigned arity = f->get_arity();
SASSERT(is_lambda(lam));
if (arity == 0)
// Constant lambda-def: just return the lambda expression
return expr_ref(lam, m);
var_subst subst(m, false);
expr_ref r = subst(lam, to_app(e)->get_num_args(), to_app(e)->get_args());
return r;
}
void ho_matcher::reduce(match_goal& wi) {
wi.pat = whnf_star(wi.pat, wi.pat_offset());
wi.t = whnf_star(wi.t, wi.term_offset());
@ -684,7 +655,7 @@ namespace euf {
}
auto is_ho = any_of(subterms::all(expr_ref(p, m)), [&](expr* t) {
return m_unitary.is_flex(0, t) ||
m.is_lambda_def(t) ||
// m.is_lambda_def(t) ||
is_lambda(t);
});
if (!is_ho)
@ -703,7 +674,8 @@ namespace euf {
todo.pop_back();
continue;
}
if ((m_unitary.is_flex(0, t) && lvl > 1) || m.is_lambda_def(t) || is_lambda(t)) {
if ((m_unitary.is_flex(0, t) && lvl > 1) || // m.is_lambda_def(t) ||
is_lambda(t)) {
if (!contains_pat2abs)
m_pat2abs.insert_if_not_there(p, svector<std::pair<unsigned, expr*>>()).push_back({ nb, t });
auto v = m.mk_var(nb++, t->get_sort());

View file

@ -355,8 +355,6 @@ namespace euf {
void reduce(match_goal& wi);
expr_ref unfold_lambda_def(expr* e) const;
trail_stack& trail() { return m_trail; }
std::ostream& display(std::ostream& out) const;

View file

@ -121,9 +121,6 @@ app * defined_names::impl::gen_name(expr * e, sort_ref_buffer & var_sorts, buffe
sort * range = e->get_sort();
func_decl * new_skolem_decl = m.mk_fresh_func_decl(m_z3name, symbol::null, domain.size(), domain.data(), range);
app * n = m.mk_app(new_skolem_decl, new_args.size(), new_args.data());
if (is_lambda(e)) {
m.add_lambda_def(new_skolem_decl, to_quantifier(e));
}
return n;
}

View file

@ -188,7 +188,7 @@ struct pull_quant::imp {
var_names.data(),
nested_q->get_expr(),
std::min(q->get_weight(), nested_q->get_weight()),
m.is_lambda_q(q) ? symbol("pulled-lambda") : q->get_qid());
q->get_qid());
}
void pull_quant1(quantifier * q, expr * new_expr, expr_ref & result) {

View file

@ -554,7 +554,7 @@ bool pattern_inference_cfg::is_forbidden(app * n) const {
// Remark: skolem constants should not be used in patterns, since they do not
// occur outside of the quantifier. That is, Z3 will never match this kind of
// pattern.
if (m_params.m_pi_avoid_skolems && decl->is_skolem() && !m.is_lambda_def(decl)) {
if (m_params.m_pi_avoid_skolems && decl->is_skolem()) {
CTRACE(pattern_inference_skolem, decl->is_skolem(), tout << "ignoring: " << mk_pp(n, m) << "\n";);
return true;
}

View file

@ -40,6 +40,7 @@ z3_add_component(rewriter
seq_axioms.cpp
seq_eq_solver.cpp
seq_derive.cpp
seq_subset.cpp
seq_rewriter.cpp
seq_skolem.cpp
th_rewriter.cpp

View file

@ -4014,51 +4014,7 @@ bool seq_rewriter::are_complements(expr* r1, expr* r2) const {
* basic subset checker.
*/
bool seq_rewriter::is_subset(expr* r1, expr* r2) const {
// return false;
expr* ra1 = nullptr, *ra2 = nullptr, *ra3 = nullptr;
expr* rb1 = nullptr, *rb2 = nullptr, *rb3 = nullptr;
unsigned la, ua, lb, ub;
if (re().is_complement(r1, ra1) &&
re().is_complement(r2, rb1)) {
return is_subset(rb1, ra1);
}
auto is_concat = [&](expr* r, expr*& a, expr*& b, expr*& c) {
return re().is_concat(r, a, b) && re().is_concat(b, b, c);
};
while (true) {
if (r1 == r2)
return true;
if (re().is_full_seq(r2))
return true;
if (re().is_dot_plus(r2) && re().get_info(r1).nullable == l_false)
return true;
if (is_concat(r1, ra1, ra2, ra3) &&
is_concat(r2, rb1, rb2, rb3) && ra1 == rb1 && ra2 == rb2) {
r1 = ra3;
r2 = rb3;
continue;
}
if (re().is_concat(r1, ra1, ra2) &&
re().is_concat(r2, rb1, rb2) && re().is_full_seq(rb1)) {
r1 = ra2;
continue;
}
// r1=ra3{la,ua}ra2, r2=rb3{lb,ub}rb2, ra3=rb3, lb<=la, ua<=ub
if (re().is_concat(r1, ra1, ra2) && re().is_loop(ra1, ra3, la, ua) &&
re().is_concat(r2, rb1, rb2) && re().is_loop(rb1, rb3, lb, ub) &&
ra3 == rb3 && lb <= la && ua <= ub) {
r1 = ra2;
r2 = rb2;
continue;
}
// ra1=ra3{la,ua}, r2=rb3{lb,ub}, ra3=rb3, lb<=la, ua<=ub
if (re().is_loop(r1, ra3, la, ua) &&
re().is_loop(r2, rb3, lb, ub) &&
ra3 == rb3 && lb <= la && ua <= ub) {
return true;
}
return false;
}
return m_subset.is_subset(r1, r2);
}
br_status seq_rewriter::mk_re_union0(expr* a, expr* b, expr_ref& result) {
@ -5628,4 +5584,3 @@ bool seq_rewriter::get_bounds(expr* e, unsigned& low, unsigned& high) {
}
return low <= high;
}

View file

@ -24,6 +24,7 @@ Notes:
#include "ast/arith_decl_plugin.h"
#include "ast/rewriter/rewriter_types.h"
#include "ast/rewriter/bool_rewriter.h"
#include "ast/rewriter/seq_subset.h"
#include "util/params.h"
#include "util/lbool.h"
#include "util/sign.h"
@ -131,6 +132,7 @@ class seq_rewriter {
friend class seq::derive;
seq_util m_util;
seq_subset m_subset;
arith_util m_autil;
bool_rewriter m_br;
seq::derive m_derive;
@ -334,7 +336,7 @@ class seq_rewriter {
public:
seq_rewriter(ast_manager & m, params_ref const & p = params_ref()):
m_util(m), m_autil(m), m_br(m, p), m_derive(m, *this), // m_re2aut(m),
m_util(m), m_subset(m_util.re), m_autil(m), m_br(m, p), m_derive(m, *this),
m_op_cache(m), m_es(m),
m_lhs(m), m_rhs(m), m_coalesce_chars(true) {
}
@ -418,4 +420,3 @@ public:
*/
lbool some_string_in_re(expr* r, zstring& s);
};

View file

@ -0,0 +1,146 @@
/*++
Copyright (c) 2026 Microsoft Corporation
Module Name:
seq_subset.cpp
Abstract:
Heuristic regular-expression subset checks used by seq_rewriter.
Author:
Nikolaj Bjorner (nbjorner) 2026-6-8
--*/
#include "ast/rewriter/seq_subset.h"
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))
return true;
if (m_re.is_full_seq(b))
return true;
if (m_re.is_epsilon(a) && m_re.get_info(b).nullable == l_true)
return true;
if (depth >= m_max_depth)
return false;
expr* a1 = nullptr, * a2 = nullptr, * b1 = nullptr, * b2 = nullptr;
unsigned la, ua, lb, ub;
// a ⊆ .+ iff a is non-nullable
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))
return true;
// R1* ⊆ R2* if R1 ⊆ R2
if (m_re.is_star(a, a1) && m_re.is_star(b, b1) && is_subset_rec(a1, b1, depth + 1))
return true;
// R1+ ⊆ R2+ if R1 ⊆ R2
if (m_re.is_plus(a, a1) && m_re.is_plus(b, b1) && is_subset_rec(a1, b1, depth))
return true;
// R ⊆ R+
if (m_re.is_plus(b, b1) && is_subset_rec(a, b1, depth))
return true;
// R+ ⊆ R*
if (m_re.is_plus(a, a1) && m_re.is_star(b, b1) && is_subset_rec(a1, b1, depth + 1))
return true;
// range containment
if (m_re.is_range(a, la, ua) && m_re.is_range(b, lb, ub) && lb <= la && ua <= ub)
return true;
// to_re(s) ⊆ range
if (m_re.is_to_re(a, a1) && m_re.is_range(b, lb, ub) && is_app(a1)) {
func_decl* f = to_app(a1)->get_decl();
if (f->get_decl_kind() == OP_STRING_CONST && f->get_num_parameters() == 1) {
zstring const& s = f->get_parameter(0).get_zstring();
if (s.length() == 1 && lb <= s[0] && s[0] <= ub)
return true;
}
}
// a ⊆ b1 b2 if a ⊆ b1 or a ⊆ b2
if (m_re.is_union(b, b1, b2) && (is_subset_rec(a, b1, depth + 1) || is_subset_rec(a, b2, depth + 1)))
return true;
// a1 a2 ⊆ b if a1 ⊆ b and a2 ⊆ b
if (m_re.is_union(a, a1, a2) && is_subset_rec(a1, b, depth + 1) && is_subset_rec(a2, b, depth + 1))
return true;
// a1 ∩ a2 ⊆ b if a1 ⊆ b or a2 ⊆ b
if (m_re.is_intersection(a, a1, a2) && (is_subset_rec(a1, b, depth + 1) || is_subset_rec(a2, b, depth + 1)))
return true;
// a ⊆ b1 ∩ b2 if a ⊆ b1 and a ⊆ b2
if (m_re.is_intersection(b, b1, b2) && is_subset_rec(a, b1, depth + 1) && is_subset_rec(a, b2, depth + 1))
return true;
// R{la,ua} ⊆ R'{lb,ub} if R ⊆ R', lb<=la, ua<=ub
if (m_re.is_loop(a, a1, la, ua) &&
m_re.is_loop(b, b1, lb, ub) &&
lb <= la && ua <= ub && is_subset_rec(a1, b1, depth + 1)) {
return true;
}
// a1 \ a2 ⊆ b if a1 ⊆ b
if (m_re.is_diff(a, a1, a2) && is_subset_rec(a1, b, depth + 1))
return true;
// R ⊆ Σ*·R' if R ⊆ R'
if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b1) && is_subset_rec(a, b2, depth))
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;
// star absorption: R·R* ⊆ R*, R*·R ⊆ R*
bool const is_concat_star = m_re.is_concat(a, a1, a2) && m_re.is_star(b, b1);
if (is_concat_star &&
is_subset_rec(a1, b1, depth + 1) && is_subset_rec(a2, b, depth + 1))
return true;
if (is_concat_star &&
is_subset_rec(a2, b1, depth + 1) && is_subset_rec(a1, b, depth + 1))
return true;
// concat monotonicity:
// tail-recursive on second arguments (without increasing depth bound).
if (m_re.is_concat(a, a1, a2) && m_re.is_concat(b, b1, b2) && is_subset_rec(a1, b1, depth + 1)) {
a = a2;
b = b2;
continue;
}
// complement: ~a ⊆ ~b if b ⊆ a
if (m_re.is_complement(a, a1) && m_re.is_complement(b, b1))
return is_subset_rec(b1, a1, depth + 1);
return false;
}
}
bool seq_subset::is_subset(expr* a, expr* b) const {
return is_subset_rec(a, b, 0);
}

View file

@ -0,0 +1,30 @@
/*++
Copyright (c) 2026 Microsoft Corporation
Module Name:
seq_subset.h
Abstract:
Heuristic regular-expression subset checks used by seq_rewriter.
Author:
Nikolaj Bjorner (nbjorner) 2026-6-8
--*/
#pragma once
#include "ast/seq_decl_plugin.h"
class seq_subset {
seq_util::rex& m_re;
static constexpr unsigned m_max_depth = 3;
bool is_subset_rec(expr* a, expr* b, unsigned depth) const;
public:
explicit seq_subset(seq_util::rex& re) : m_re(re) {}
bool is_subset(expr* a, expr* b) const;
};

View file

@ -88,22 +88,6 @@ void dependent_expr_state::freeze_recfun() {
m_num_recfun = sz;
}
/**
* Freeze all functions used in lambda defined declarations
*/
void dependent_expr_state::freeze_lambda() {
auto& m = m_frozen_trail.get_manager();
unsigned sz = m.lambda_defs().size();
if (m_num_lambdas >= sz)
return;
ast_mark visited;
for (auto const& [f, body] : m.lambda_defs())
freeze_terms(body, false, visited);
m_trail.push(value_trail(m_num_lambdas));
m_num_lambdas = sz;
}
/**
* The current qhead is to be updated to qtail.
@ -122,8 +106,7 @@ void dependent_expr_state::freeze_suffix() {
if (m_suffix_frozen)
return;
m_suffix_frozen = true;
freeze_recfun();
freeze_lambda();
freeze_recfun();
auto& m = m_frozen_trail.get_manager();
ast_mark visited;
ptr_vector<expr> es;

View file

@ -51,7 +51,6 @@ class dependent_expr_state {
func_decl_ref_vector m_frozen_trail;
void freeze_prefix();
void freeze_recfun();
void freeze_lambda();
void freeze_terms(expr* term, bool only_as_array, ast_mark& visited);
void freeze(func_decl* f);
struct thaw : public trail {

View file

@ -172,9 +172,7 @@ namespace sls {
return false;
if (r > sx.length() && update(x, sx + zstring(random_char())))
return false;
// This case seems to imply unsat
verbose_stream() << "The input might be unsat\n"; // example to trigger: (assert (and (>= (str.len X) 2) (= (str.substr X 0 1) "")))
VERIFY(false);
// Both updates failed. Treat as unsatisfied and let outer search continue.
return false;
}
@ -198,8 +196,16 @@ namespace sls {
return false;
}
if (seq.str.is_last_index(e, x, y) && seq.is_string(x->get_sort())) {
// TODO
NOT_IMPLEMENTED_YET();
auto sx = strval0(x);
auto sy = strval0(y);
rational val_e;
if (!a.is_numeral(ctx.get_value(e), val_e))
return false;
rational actual(sx.last_indexof(sy));
if (val_e == actual)
continue;
update(e, actual);
return false;
}
if (seq.str.is_stoi(e, x) && seq.is_string(x->get_sort())) {
auto sx = strval0(x);
@ -753,7 +759,7 @@ namespace sls {
for (unsigned j = 1; j <= val_other.length() - i; ++j) {
zstring sub = val_other.extract(i, j);
if (set.contains(sub))
break;
continue;
set.insert(sub);
}
}
@ -906,7 +912,7 @@ namespace sls {
m_string_updates.reset();
u[i][j] = d[i - 1][j];
}
if (d[i][j - 1] < u[i][j] && b.can_add(i - 1)) {
if (d[i][j - 1] < u[i][j] && b.can_add(j - 1)) {
m_string_updates.reset();
u[i][j] = d[i][j - 1];
}