mirror of
https://github.com/Z3Prover/z3
synced 2026-05-27 20:36:26 +00:00
[WIP] Try to replace "recursive reusage" of variables by seq.slice
This commit is contained in:
parent
dd00dd7362
commit
315a09aea8
7 changed files with 88 additions and 156 deletions
|
|
@ -321,7 +321,7 @@ namespace seq {
|
|||
// We have to add all - even if some of it conflict
|
||||
// [otw. we would leave the node partially initialized]
|
||||
for (const auto f : *to_app(c.fml)) {
|
||||
add_constraint(constraint(f, c.dep, m));
|
||||
add_constraint(constraint(f, c.dep, c.internal, m));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -371,8 +371,7 @@ namespace seq {
|
|||
expr* var_c_expr = s.m_var->arg(0)->get_expr();
|
||||
expr* repl_c_expr = s.m_replacement->arg(0)->get_expr();
|
||||
add_constraint(
|
||||
constraint(
|
||||
m.mk_eq(var_c_expr, repl_c_expr), s.m_dep, m));
|
||||
constraint(m.mk_eq(var_c_expr, repl_c_expr), s.m_dep, false, m));
|
||||
|
||||
if (m_char_ranges.contains(var_id)) {
|
||||
const auto range = m_char_ranges.find(var_id); // copy exactly
|
||||
|
|
@ -426,7 +425,7 @@ namespace seq {
|
|||
seq.mk_le(seq.mk_char(ranges[i].m_lo), var),
|
||||
seq.mk_le(var, seq.mk_char(ranges[i].m_hi - 1)));
|
||||
}
|
||||
add_constraint(constraint(m.mk_or(cases), dep, m));
|
||||
add_constraint(constraint(m.mk_or(cases), dep, false, m));
|
||||
}
|
||||
// -----------------------------------------------
|
||||
// nielsen_graph
|
||||
|
|
@ -519,16 +518,6 @@ namespace seq {
|
|||
m_root->add_str_mem(str_mem(str, regex, dep));
|
||||
}
|
||||
|
||||
void nielsen_graph::inc_run_idx() {
|
||||
if (m_run_idx == UINT_MAX) {
|
||||
for (nielsen_node* n : m_nodes)
|
||||
n->reset_counter();
|
||||
m_run_idx = 1;
|
||||
}
|
||||
else
|
||||
++m_run_idx;
|
||||
}
|
||||
|
||||
void nielsen_graph::reset() {
|
||||
for (nielsen_node* n : m_nodes)
|
||||
dealloc(n);
|
||||
|
|
@ -539,7 +528,6 @@ namespace seq {
|
|||
m_root = nullptr;
|
||||
m_sat_node = nullptr;
|
||||
m_sat_path.reset();
|
||||
m_run_idx = 0;
|
||||
m_depth_bound = 0;
|
||||
m_fresh_cnt = 0;
|
||||
m_root_constraints_asserted = false;
|
||||
|
|
@ -567,7 +555,7 @@ namespace seq {
|
|||
// just assume it to be correct
|
||||
// Just add the constraint - we do not have to recompute it
|
||||
// [also it is on the set of side-conditions if we assert a satisfied node]
|
||||
n->add_constraint(constraint(le, dep, m));
|
||||
n->add_constraint(constraint(le, dep, false, m));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -1708,7 +1696,6 @@ namespace seq {
|
|||
}
|
||||
if (m_max_search_depth > 0 && m_depth_bound > m_max_search_depth)
|
||||
break;
|
||||
inc_run_idx();
|
||||
ptr_vector<nielsen_edge> cur_path;
|
||||
// scoped_push _scoped_push(m_dep_mgr); // gc dependencies after search
|
||||
const search_result r = search_dfs(m_root, cur_path); // the main search loop
|
||||
|
|
@ -1783,29 +1770,17 @@ namespace seq {
|
|||
if (m_max_nodes > 0 && m_stats.m_num_dfs_nodes > m_max_nodes)
|
||||
return search_result::unknown;
|
||||
|
||||
// revisit detection: if already visited this run (e.g., iterative deepening), return cached status.
|
||||
// mirrors ZIPT's NielsenNode.GraphExpansion() evalIdx check.
|
||||
const unsigned eval_idx = node->eval_idx();
|
||||
if (eval_idx == m_run_idx) {
|
||||
if (node->is_satisfied()) {
|
||||
m_sat_node = node;
|
||||
m_sat_path = cur_path;
|
||||
return search_result::sat;
|
||||
}
|
||||
if (node->is_currently_conflict())
|
||||
return search_result::unsat;
|
||||
return search_result::unknown;
|
||||
if (node->is_satisfied()) {
|
||||
m_sat_node = node;
|
||||
m_sat_path = cur_path;
|
||||
return search_result::sat;
|
||||
}
|
||||
node->set_eval_idx(m_run_idx);
|
||||
SASSERT(!node->is_general_conflict());
|
||||
if (eval_idx > 0)
|
||||
// adding the side-conditions from the edge
|
||||
// when generating this node might trigger a local conflict
|
||||
node->clear_local_conflict(); // clear local conflicts from previous runs
|
||||
if (node->is_currently_conflict())
|
||||
return search_result::unsat;
|
||||
|
||||
// we might need to tell the SAT solver about the new integer inequalities
|
||||
// that might have been added by an extension step
|
||||
assert_node_new_int_constraints(node);
|
||||
assert_node_side_constraints(node);
|
||||
|
||||
if (node->is_currently_conflict()) {
|
||||
++m_stats.m_num_simplify_conflict;
|
||||
|
|
@ -1841,7 +1816,7 @@ namespace seq {
|
|||
// Also generate per-node |LHS| = |RHS| length constraints for descendant
|
||||
// equations (root constraints are already at the base level).
|
||||
generate_node_length_constraints(node);
|
||||
assert_node_new_int_constraints(node);
|
||||
assert_node_side_constraints(node);
|
||||
|
||||
if (node->is_currently_conflict()) {
|
||||
++m_stats.m_num_simplify_conflict;
|
||||
|
|
@ -1873,7 +1848,7 @@ namespace seq {
|
|||
node->set_conflict(backtrack_reason::regex, dep);
|
||||
return search_result::unsat;
|
||||
}
|
||||
assert_node_new_int_constraints(node);
|
||||
assert_node_side_constraints(node);
|
||||
// We need to have everything asserted before reporting SAT
|
||||
// (otw. the outer solver might assume false-assigned literals to be true)
|
||||
if (node->is_currently_conflict()) {
|
||||
|
|
@ -3971,7 +3946,7 @@ namespace seq {
|
|||
nielsen_node *child = mk_child(node);
|
||||
nielsen_edge* e = mk_edge(node, child, true);
|
||||
for (const auto f : cs) {
|
||||
e->add_side_constraint(constraint(f, mem.m_dep, m));
|
||||
e->add_side_constraint(constraint(f, mem.m_dep, false, m));
|
||||
}
|
||||
for (str_mem &cm : child->str_mems()) {
|
||||
if (cm == mem) {
|
||||
|
|
@ -4281,11 +4256,21 @@ namespace seq {
|
|||
|
||||
dep_tracker nielsen_graph::collect_conflict_deps() const {
|
||||
dep_tracker deps = nullptr;
|
||||
// We enumerate all nodes rather than having a "todo"-list, as
|
||||
// hypothetically the graph could contain cycles in the future
|
||||
for (nielsen_node const* n : m_nodes) {
|
||||
// todo: Add visit set if the graph could contain cycles in the future
|
||||
// enumerating all nodes would not work due to hot-restarts having created
|
||||
// children that are currently not relevant
|
||||
vector<nielsen_node const*> to_visit;
|
||||
to_visit.push_back(m_root);
|
||||
while (!to_visit.empty()) {
|
||||
nielsen_node const* n = to_visit.back();
|
||||
to_visit.pop_back();
|
||||
if (n->reason() == backtrack_reason::children_failed) {
|
||||
SASSERT(n->m_conflict_external_literal == sat::null_literal);
|
||||
SASSERT(!n->m_conflict_internal);
|
||||
for (unsigned i = n->outgoing().size(); i > 0; i--) {
|
||||
nielsen_edge const* e = n->outgoing()[i - 1];
|
||||
to_visit.push_back(e->tgt());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// not true anymore since we might have done a hot-restart where we previously created the child:
|
||||
|
|
@ -4362,7 +4347,7 @@ namespace seq {
|
|||
tout << "Length constraint from LHS " << snode_label_html(eq.m_lhs, m) << " to " << len_lhs << ":\n";
|
||||
tout << "Length constraint from RHS " << snode_label_html(eq.m_rhs, m) << " to " << len_rhs << "\n");
|
||||
expr_ref len_eq(m.mk_eq(len_lhs, len_rhs), m);
|
||||
constraints.push_back(length_constraint(len_eq, eq.m_dep, length_kind::eq, m));
|
||||
constraints.push_back(length_constraint(len_eq, eq.m_dep, length_kind::eq, true, m));
|
||||
|
||||
// collect variables for non-negativity constraints
|
||||
euf::snode_vector tokens;
|
||||
|
|
@ -4374,7 +4359,7 @@ namespace seq {
|
|||
expr_ref len_var(seq.str.mk_length(tok->get_expr()), m);
|
||||
expr_ref ge_zero(a.mk_ge(len_var, a.mk_int(0)), m);
|
||||
TRACE(seq, tout << "non-negative length " << ge_zero << "\n");
|
||||
constraints.push_back(length_constraint(ge_zero, eq.m_dep, length_kind::nonneg, m));
|
||||
constraints.push_back(length_constraint(ge_zero, eq.m_dep, length_kind::nonneg, true, m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4393,14 +4378,14 @@ namespace seq {
|
|||
if (min_len > 0) {
|
||||
expr_ref bound(a.mk_ge(len_str, a.mk_int(min_len)), m);
|
||||
TRACE(seq, tout << "Parikh " << mk_pp(re_expr, m) << " bound: " << bound << "\n");
|
||||
constraints.push_back(length_constraint(bound, mem.m_dep, length_kind::bound, m));
|
||||
constraints.push_back(length_constraint(bound, mem.m_dep, length_kind::bound, false, m));
|
||||
}
|
||||
|
||||
// generate len(str) <= max_len when bounded
|
||||
if (max_len < UINT_MAX) {
|
||||
expr_ref bound(a.mk_le(len_str, a.mk_int(max_len)), m);
|
||||
TRACE(seq, tout << "Parikh " << mk_pp(re_expr, m) << " bound: " << bound << "\n");
|
||||
constraints.push_back(length_constraint(bound, mem.m_dep, length_kind::bound, m));
|
||||
constraints.push_back(length_constraint(bound, mem.m_dep, length_kind::bound, false, m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4482,7 +4467,7 @@ namespace seq {
|
|||
continue;
|
||||
has_non_elim = true;
|
||||
// Assert LHS >= 0
|
||||
e->add_side_constraint(mk_constraint(a.mk_ge(lhs, a.mk_int(0)), s.m_dep));
|
||||
e->add_side_constraint(mk_constraint(a.mk_ge(lhs, a.mk_int(0)), s.m_dep, true));
|
||||
}
|
||||
|
||||
// Step 2: Bump mod counts for all non-eliminating variables at once.
|
||||
|
|
@ -4494,7 +4479,13 @@ namespace seq {
|
|||
for (auto const &[idx, lhs] : lhs_exprs) {
|
||||
auto const& s = substs[idx];
|
||||
expr_ref rhs = compute_length_expr(s.m_replacement);
|
||||
e->add_side_constraint(mk_constraint(m.mk_eq(lhs, rhs), s.m_dep));
|
||||
e->add_side_constraint(mk_constraint(m.mk_eq(lhs, rhs), s.m_dep, true));
|
||||
// otw. we have equality as a side-constraint in the Nielsen node
|
||||
// and adding the Nielsen assumption will force the phase of the equality to true
|
||||
// however the arith.-solver will axiomatize it as <= && >= which do not have a phase
|
||||
// hence it might get assigned false by decision which is a performance problem
|
||||
// e->add_side_constraint(mk_constraint(a.mk_le(lhs, rhs), s.m_dep));
|
||||
// e->add_side_constraint(mk_constraint(a.mk_ge(lhs, rhs), s.m_dep));
|
||||
}
|
||||
|
||||
// Step 4: Restore mod counts (temporary bump for computing RHS only).
|
||||
|
|
@ -4547,7 +4538,7 @@ namespace seq {
|
|||
m_length_solver.assert_expr(e);
|
||||
}
|
||||
|
||||
void nielsen_graph::assert_node_new_int_constraints(nielsen_node* node) const {
|
||||
void nielsen_graph::assert_node_side_constraints(nielsen_node* node) const {
|
||||
// Assert only the constraints that are new to this node (beyond those
|
||||
// inherited from its parent via clone_from). The parent's constraints are
|
||||
// already present in the enclosing solver scope; asserting them again would
|
||||
|
|
@ -4585,7 +4576,7 @@ namespace seq {
|
|||
|
||||
expr_ref len_lhs = compute_length_expr(eq.m_lhs);
|
||||
expr_ref len_rhs = compute_length_expr(eq.m_rhs);
|
||||
node->add_constraint(mk_constraint(m.mk_eq(len_lhs, len_rhs), eq.m_dep));
|
||||
node->add_constraint(mk_constraint(m.mk_eq(len_lhs, len_rhs), eq.m_dep, true));
|
||||
|
||||
// non-negativity for each variable (mod-count-aware)
|
||||
euf::snode_vector tokens;
|
||||
|
|
@ -4595,7 +4586,7 @@ namespace seq {
|
|||
if (tok->is_var() && !seen_vars.contains(tok->id())) {
|
||||
seen_vars.insert(tok->id());
|
||||
expr_ref len_var = compute_length_expr(tok);
|
||||
node->add_constraint(mk_constraint(a.mk_ge(len_var, a.mk_int(0)), eq.m_dep));
|
||||
node->add_constraint(mk_constraint(a.mk_ge(len_var, a.mk_int(0)), eq.m_dep, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4611,9 +4602,9 @@ namespace seq {
|
|||
expr_ref len_str = compute_length_expr(mem.m_str);
|
||||
|
||||
if (min_len > 0)
|
||||
node->add_constraint(mk_constraint(a.mk_ge(len_str, a.mk_int(min_len)), mem.m_dep));
|
||||
node->add_constraint(mk_constraint(a.mk_ge(len_str, a.mk_int(min_len)), mem.m_dep, true));
|
||||
if (max_len < UINT_MAX)
|
||||
node->add_constraint(mk_constraint(a.mk_le(len_str, a.mk_int(max_len)), mem.m_dep));
|
||||
node->add_constraint(mk_constraint(a.mk_le(len_str, a.mk_int(max_len)), mem.m_dep, true));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4674,14 +4665,14 @@ namespace seq {
|
|||
dep = m_length_solver.core();
|
||||
m_length_solver.pop(1);
|
||||
if (result == l_false) {
|
||||
n->add_constraint(constraint(a.mk_le(lhs, rhs), dep, m));
|
||||
n->add_constraint(constraint(a.mk_le(lhs, rhs), dep, false, m));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constraint nielsen_graph::mk_constraint(expr* fml, dep_tracker const& dep) const {
|
||||
return constraint(fml, dep, m);
|
||||
constraint nielsen_graph::mk_constraint(expr* fml, dep_tracker const& dep, bool internal) const {
|
||||
return constraint(fml, dep, internal, m);
|
||||
}
|
||||
|
||||
expr* nielsen_graph::get_power_exponent(euf::snode* power) {
|
||||
|
|
|
|||
|
|
@ -499,6 +499,7 @@ namespace seq {
|
|||
struct constraint {
|
||||
expr_ref fml; // the formula (eq, le, or ge, unit-diseq expression)
|
||||
dep_tracker dep; // tracks which input constraints contributed
|
||||
bool internal; // don't push back to the outer solver (helpful if not necessary and it would reveal internal variables)
|
||||
|
||||
static expr_ref simplify(expr* f, ast_manager& m) {
|
||||
//th_rewriter th(m);
|
||||
|
|
@ -508,9 +509,10 @@ namespace seq {
|
|||
}
|
||||
|
||||
constraint(ast_manager& m):
|
||||
fml(m), dep(nullptr) {}
|
||||
constraint(expr* f, dep_tracker const& d, ast_manager& m):
|
||||
fml(simplify(f, m)), dep(d) {}
|
||||
fml(m), dep(nullptr), internal(true) {}
|
||||
|
||||
constraint(expr* f, dep_tracker const& d, const bool internal, ast_manager& m):
|
||||
fml(simplify(f, m)), dep(d), internal(internal) {}
|
||||
|
||||
std::ostream& display(std::ostream& out) const;
|
||||
};
|
||||
|
|
@ -527,13 +529,14 @@ namespace seq {
|
|||
expr_ref m_expr; // arithmetic expression (e.g., len(x) + len(y) = len(a) + 1)
|
||||
dep_tracker m_dep; // tracks which input constraints contributed
|
||||
length_kind m_kind; // determines propagation strategy
|
||||
bool m_internal;
|
||||
|
||||
length_constraint(ast_manager& m): m_expr(m), m_dep(nullptr), m_kind(length_kind::nonneg) {}
|
||||
length_constraint(expr* e, dep_tracker const& dep, length_kind kind, ast_manager& m):
|
||||
m_expr(e, m), m_dep(dep), m_kind(kind) {}
|
||||
length_constraint(ast_manager& m): m_expr(m), m_dep(nullptr), m_kind(length_kind::nonneg), m_internal(true) {}
|
||||
length_constraint(expr* e, dep_tracker const& dep, length_kind kind, const bool internal, ast_manager& m):
|
||||
m_expr(e, m), m_dep(dep), m_kind(kind), m_internal(internal) {}
|
||||
|
||||
constraint to_constraint() const {
|
||||
return constraint(m_expr, m_dep, m_expr.get_manager());
|
||||
return constraint(m_expr, m_dep, m_internal, m_expr.get_manager());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -603,9 +606,6 @@ namespace seq {
|
|||
bool m_is_progress = false;
|
||||
bool m_node_len_constraints_generated = false; // true after generate_node_length_constraints runs
|
||||
|
||||
// evaluation index for run tracking
|
||||
unsigned m_eval_idx = 0;
|
||||
|
||||
// Parikh filter: set to true once apply_parikh_to_node has been applied
|
||||
// to this node. Prevents duplicate constraint generation across DFS runs.
|
||||
bool m_parikh_applied = false;
|
||||
|
|
@ -724,10 +724,6 @@ namespace seq {
|
|||
|
||||
bool is_progress() const { return m_is_progress; }
|
||||
|
||||
unsigned eval_idx() const { return m_eval_idx; }
|
||||
void set_eval_idx(unsigned idx) { m_eval_idx = idx; }
|
||||
void reset_counter() { m_eval_idx = 0; }
|
||||
|
||||
// clone constraints from a parent node
|
||||
void clone_from(nielsen_node const& parent);
|
||||
|
||||
|
|
@ -849,7 +845,6 @@ namespace seq {
|
|||
nielsen_node* m_root = nullptr;
|
||||
nielsen_node* m_sat_node = nullptr;
|
||||
ptr_vector<nielsen_edge> m_sat_path;
|
||||
unsigned m_run_idx = 0;
|
||||
unsigned m_depth_bound = 0;
|
||||
unsigned m_max_search_depth = 0;
|
||||
unsigned m_max_nodes = 0; // 0 = unlimited
|
||||
|
|
@ -969,10 +964,6 @@ namespace seq {
|
|||
void add_str_eq(euf::snode* lhs, euf::snode* rhs);
|
||||
void add_str_mem(euf::snode* str, euf::snode* regex);
|
||||
|
||||
// run management
|
||||
unsigned run_idx() const { return m_run_idx; }
|
||||
void inc_run_idx();
|
||||
|
||||
// access all nodes
|
||||
ptr_vector<nielsen_node> const& nodes() const { return m_nodes; }
|
||||
unsigned num_nodes() const { return m_nodes.size(); }
|
||||
|
|
@ -1065,7 +1056,7 @@ namespace seq {
|
|||
// parent (indices [m_parent_ic_count..end)) into the current solver scope.
|
||||
// Called by search_dfs after simplify_and_init so that the newly derived
|
||||
// bounds become visible to subsequent check() and check_lp_le() calls.
|
||||
void assert_node_new_int_constraints(nielsen_node* node) const;
|
||||
void assert_node_side_constraints(nielsen_node* node) const;
|
||||
|
||||
private:
|
||||
|
||||
|
|
@ -1316,7 +1307,7 @@ namespace seq {
|
|||
bool check_lp_le(expr* lhs, expr* rhs, nielsen_node* n, dep_tracker& dep);
|
||||
|
||||
// create an integer constraint: lhs <kind> rhs
|
||||
constraint mk_constraint(expr* fml, dep_tracker const& dep) const;
|
||||
constraint mk_constraint(expr* fml, dep_tracker const& dep, bool internal = false) const;
|
||||
|
||||
// get the exponent expression from a power snode (arg(1))
|
||||
static expr * get_power_exponent(euf::snode* power);
|
||||
|
|
|
|||
|
|
@ -537,9 +537,10 @@ namespace seq {
|
|||
out << "<br/>";
|
||||
}
|
||||
// integer constraints
|
||||
std::vector<std::string> int_constraints(m_constraints.size());
|
||||
std::vector<std::string> int_constraints;
|
||||
for (auto const& ic : m_constraints) {
|
||||
int_constraints.push_back(constraint_html(ic, names, next_id, m));
|
||||
int_constraints.push_back(
|
||||
(ic.internal ? "[I] " : "") + constraint_html(ic, names, next_id, m));
|
||||
}
|
||||
if (!int_constraints.empty()) {
|
||||
// eliminate duplicates
|
||||
|
|
@ -635,8 +636,6 @@ namespace seq {
|
|||
out << ", color=darkred";
|
||||
else if (n->is_currently_conflict())
|
||||
out << ", color=red";
|
||||
else if (n->eval_idx() != m_run_idx) // inactive, not visited this run
|
||||
out << ", color=blue";
|
||||
|
||||
out << "];\n";
|
||||
}
|
||||
|
|
@ -669,8 +668,6 @@ namespace seq {
|
|||
nielsen_edge* ep = const_cast<nielsen_edge*>(e);
|
||||
if (sat_edges.contains(ep))
|
||||
out << ", color=green";
|
||||
else if (e->tgt()->eval_idx() != m_run_idx)
|
||||
out << ", color=blue";
|
||||
else if (e->tgt()->is_currently_conflict())
|
||||
out << ", color=red";
|
||||
|
||||
|
|
|
|||
|
|
@ -210,11 +210,11 @@ namespace seq {
|
|||
expr_ref stride_expr(a.mk_int(stride), m);
|
||||
expr_ref stride_k(a.mk_mul(stride_expr, k_var), m);
|
||||
expr_ref rhs(a.mk_add(min_expr, stride_k), m);
|
||||
out.push_back(constraint(m.mk_eq(len_str, rhs), mem.m_dep, m));
|
||||
out.push_back(constraint(m.mk_eq(len_str, rhs), mem.m_dep, false, m));
|
||||
|
||||
// Constraint 2: k ≥ 0
|
||||
expr_ref zero(a.mk_int(0), m);
|
||||
out.push_back(constraint(a.mk_ge(k_var, zero), mem.m_dep, m));
|
||||
out.push_back(constraint(a.mk_ge(k_var, zero), mem.m_dep, false, m));
|
||||
|
||||
// Constraint 3 (optional): k ≤ max_k when max_len is bounded.
|
||||
// max_k = floor((max_len - min_len) / stride)
|
||||
|
|
@ -226,7 +226,7 @@ namespace seq {
|
|||
unsigned range = max_len - min_len;
|
||||
unsigned max_k = range / stride;
|
||||
expr_ref max_k_expr(a.mk_int(max_k), m);
|
||||
out.push_back(constraint(a.mk_le(k_var, max_k_expr), mem.m_dep, m));
|
||||
out.push_back(constraint(a.mk_le(k_var, max_k_expr), mem.m_dep, false, m));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue