use super::*;
const LOOP_HEADER_SELECTION_PREFIX: &str = "%metal2vulkan.lhsel.";
pub(in crate::native) fn refine_loop_exit_selection_merges(
blocks: &[BodyBlock],
forest: &LoopForest,
loop_merges: &HashMap<String, LoopMergeInfo>,
selection_merges: &mut HashMap<String, String>,
) {
if blocks.len() > LOOP_EXIT_SELECTION_MAX_BLOCKS {
return;
}
let headers: Vec<(String, String)> = selection_merges
.iter()
.map(|(header, merge)| (header.clone(), merge.clone()))
.collect();
for (header, natural) in headers {
let mut enclosing: Vec<_> = forest
.loops
.iter()
.filter(|loop_info| loop_info.header != header)
.filter(|loop_info| loop_info.body.iter().any(|node| node == &header))
.filter_map(|loop_info| {
loop_merges
.get(&loop_info.header)
.filter(|info| info.merge == natural)
.map(|info| (loop_info, info))
})
.collect();
enclosing.sort_by_key(|(loop_info, _)| loop_info.body.len());
for (loop_info, info) in enclosing {
if let Some(convergence) = loop_exit_selection_convergence(
blocks,
forest,
loop_merges,
loop_info,
info,
&header,
) {
selection_merges.insert(header.clone(), convergence);
break;
}
}
}
}
pub(in crate::native) fn loop_exit_selection_convergence(
blocks: &[BodyBlock],
forest: &LoopForest,
loop_merges: &HashMap<String, LoopMergeInfo>,
loop_info: &super::loopforest::NaturalLoop,
info: &LoopMergeInfo,
header: &str,
) -> Option<String> {
let header_block = blocks.iter().find(|block| block.name == header)?;
let arms = block_successors(header_block);
if arms.len() < 2 {
return None;
}
let loop_body: HashSet<&str> = loop_info.body.iter().map(String::as_str).collect();
let nested_loop_nodes: HashSet<&str> = forest
.loops
.iter()
.filter(|other| other.header != loop_info.header)
.filter(|other| loop_body.contains(other.header.as_str()))
.flat_map(|other| other.body.iter().map(String::as_str))
.collect();
let all_loop_roles: HashSet<&str> = loop_merges
.values()
.flat_map(|role| [role.merge.as_str(), role.continue_target.as_str()])
.collect();
let mut candidates: Vec<&str> = blocks
.iter()
.map(|block| block.name.as_str())
.filter(|candidate| {
*candidate != header
&& *candidate != loop_info.header
&& *candidate != info.merge
&& *candidate != info.continue_target
&& loop_body.contains(candidate)
&& !nested_loop_nodes.contains(candidate)
&& !all_loop_roles.contains(candidate)
})
.collect();
let depth = |name: &str| {
let mut depth = 0usize;
let mut cur = name;
while let Some(parent) = forest.idom(cur) {
depth += 1;
cur = parent;
}
depth
};
candidates.sort_by_key(|candidate| (depth(candidate), *candidate));
let mut best: Option<(usize, usize, &str)> = None;
for candidate in candidates {
let mut valid = true;
let mut reaching_arms = 0usize;
for arm in &arms {
let mut reached = false;
let mut seen: HashSet<String> = HashSet::new();
let mut stack = vec![arm.clone()];
while let Some(node) = stack.pop() {
if node == candidate {
reached = true;
continue;
}
if node == info.merge || node == info.continue_target || node == loop_info.header {
continue;
}
if !seen.insert(node.clone()) {
continue;
}
if !loop_body.contains(node.as_str()) || nested_loop_nodes.contains(node.as_str()) {
valid = false;
break;
}
let Some(block) = blocks.iter().find(|block| block.name == node) else {
valid = false;
break;
};
let successors = block_successors(block);
if successors.is_empty() {
valid = false;
break;
}
stack.extend(successors);
}
if !valid {
valid = false;
break;
}
reaching_arms += reached as usize;
}
if valid && reaching_arms > 0 {
let score = (reaching_arms, depth(candidate), candidate);
if best
.as_ref()
.is_none_or(|current| score > (current.0, current.1, current.2))
{
best = Some(score);
}
}
}
best.map(|(_, _, candidate)| candidate.to_string())
}
pub(in crate::native) fn synth_unique_selection_merge(
blocks: &mut Vec<BodyBlock>,
forest: &LoopForest,
header: &str,
natural: &str,
counter: &mut usize,
) -> Option<String> {
let preds: Vec<String> = blocks
.iter()
.filter(|b| {
b.role != BlockRole::ConstructTreeRoute
&& forest.dominates(header, &b.name)
&& block_successors(b).iter().any(|s| s == natural)
})
.map(|b| b.name.clone())
.collect();
if preds.is_empty() {
return None;
}
let new_name = format!("{SPLIT_PREFIX}{SEL_TOKEN}{counter}");
*counter += 1;
for b in blocks.iter_mut() {
if preds.iter().any(|p| p == &b.name) {
if let Some(t) = &mut b.typed {
t.redirect_successor(natural, &new_name);
}
}
}
let at = blocks
.iter()
.position(|b| b.name == natural)
.unwrap_or(blocks.len());
blocks.insert(
at,
synthetic_block(
new_name.clone(),
vec![format!("br label {natural}")],
role_for_name(&new_name),
),
);
Some(new_name)
}
pub(in crate::native) fn synth_unique_selection_merge_phi(
blocks: &mut Vec<BodyBlock>,
forest: &LoopForest,
header: &str,
natural: &str,
counter: &mut usize,
) -> Option<String> {
let preds: Vec<String> = blocks
.iter()
.filter(|b| {
b.role != BlockRole::ConstructTreeRoute
&& forest.dominates(header, &b.name)
&& block_successors(b).iter().any(|s| s == natural)
})
.map(|b| b.name.clone())
.collect();
if preds.is_empty() {
return None;
}
let is_redirected = |pred: &str| preds.iter().any(|p| p == pred);
let new_name = format!("{SPLIT_PREFIX}{SEL_TOKEN}{counter}");
*counter += 1;
let nat_idx = blocks.iter().position(|b| b.name == natural)?;
type TypedIncomings = Vec<(crate::native::ir::LlValue, String)>;
let mut passthrough_merges: Vec<(String, crate::native::ir::LlType, TypedIncomings)> =
Vec::new();
let mut nat_rewrites: Vec<(String, TypedIncomings)> = Vec::new();
if let Some(t) = &blocks[nat_idx].typed {
for inst in &t.insts {
let (Some(dst), Some((ty, inc))) = (inst.result.clone(), inst.phi_incoming.clone())
else {
continue;
};
let (typed_red, mut kept_plus): (Vec<_>, Vec<_>) =
inc.into_iter().partition(|(_, pred)| is_redirected(pred));
if typed_red.is_empty() {
continue;
}
let merged = format!("{new_name}.phi{}", passthrough_merges.len());
kept_plus.push((
crate::native::ir::LlValue::Local(merged.clone()),
new_name.clone(),
));
passthrough_merges.push((merged, ty, typed_red));
nat_rewrites.push((dst, kept_plus));
}
}
if let Some(t) = &mut blocks[nat_idx].typed {
for (dst, kept_plus) in &nat_rewrites {
t.set_phi_incomings(dst, kept_plus);
}
}
for b in blocks.iter_mut() {
if preds.iter().any(|p| p == &b.name) {
if let Some(t) = &mut b.typed {
t.redirect_successor(natural, &new_name);
}
}
}
let at = blocks
.iter()
.position(|b| b.name == natural)
.unwrap_or(blocks.len());
let mut blk = crate::native::tir::lower_block_carrier(
&new_name,
&[format!("br label {natural}")],
&std::collections::HashMap::new(),
)?;
for (merged, ty, typed_red) in &passthrough_merges {
blk.push_value_phi(merged, ty, typed_red);
}
blocks.insert(
at,
BodyBlock {
name: new_name.clone(),
role: role_for_name(&new_name),
typed: Some(blk),
},
);
Some(new_name)
}
pub(in crate::native) fn merge_collides_with_outer_selection(
blocks: &[BodyBlock],
header: &str,
merge: &str,
converge_inloop: bool,
) -> bool {
let forest = analyze(blocks);
let Some(l) = forest.loop_for_header(header) else {
return false;
};
let body: HashSet<&str> = l.body.iter().map(String::as_str).collect();
let sel = selection_merges(blocks, &forest);
if sel
.iter()
.any(|(h, m)| m == merge && !body.contains(h.as_str()))
{
return true;
}
if converge_inloop {
return sel
.iter()
.any(|(h, m)| m == merge && body.contains(h.as_str()) && h.as_str() != header);
}
false
}
pub(in crate::native) fn split_no_phi_overlap(
blocks: &mut Vec<BodyBlock>,
forest: &LoopForest,
header: &str,
exit: &str,
counter: &mut usize,
) -> Option<String> {
let body: Vec<String> = forest.loop_for_header(header)?.body.clone();
let preds: Vec<String> = body
.iter()
.filter(|name| {
blocks
.iter()
.find(|b| &b.name == *name)
.map(|b| block_successors(b).iter().any(|s| s == exit))
.unwrap_or(false)
})
.cloned()
.collect();
if preds.is_empty() {
return None;
}
let new_name = format!("{SPLIT_PREFIX}{counter}");
*counter += 1;
for b in blocks.iter_mut() {
if preds.iter().any(|p| p == &b.name) {
if let Some(t) = &mut b.typed {
t.redirect_successor(exit, &new_name);
}
}
}
let pass_through = synthetic_block(
new_name.clone(),
vec![format!("br label {exit}")],
role_for_name(&new_name),
);
let insert_at = blocks
.iter()
.position(|b| b.name == exit)
.unwrap_or(blocks.len());
blocks.insert(insert_at, pass_through);
Some(new_name)
}
pub(in crate::native) fn synth_dowhile_continue(
blocks: &mut Vec<BodyBlock>,
header: &str,
latch: &str,
merge: &str,
counter: &mut usize,
) -> Option<String> {
let (t, f) = {
let lb = blocks.iter().find(|b| b.name == latch)?;
conditional_branch_targets(lb)?
};
let arms = [t.as_str(), f.as_str()];
if !(arms.contains(&header) && arms.contains(&merge)) {
return None;
}
let cont = format!("{CONT_PREFIX}{counter}");
*counter += 1;
if let Some(lb) = blocks.iter_mut().find(|b| b.name == latch) {
if let Some(t) = &mut lb.typed {
t.redirect_successor(header, &cont);
}
}
if let Some(hb) = blocks.iter_mut().find(|b| b.name == header) {
if let Some(t) = &mut hb.typed {
t.rewrite_phi_predecessor(latch, &cont);
}
}
let at = blocks
.iter()
.position(|b| b.name == header)
.unwrap_or(blocks.len());
blocks.insert(
at,
synthetic_block(
cont.clone(),
vec![format!("br label {header}")],
role_for_name(&cont),
),
);
Some(cont)
}
pub(in crate::native) fn split_loop_header_selection(
blocks: &mut Vec<BodyBlock>,
header: &str,
merge: &str,
continue_target: &str,
counter: &mut usize,
) -> Option<String> {
let (t, f) = {
let hb = blocks.iter().find(|b| b.name == header)?;
conditional_branch_targets(hb)?
};
let roles = [merge, continue_target, header];
if roles.contains(&t.as_str()) || roles.contains(&f.as_str()) {
return None;
}
let sel = format!("{LOOP_HEADER_SELECTION_PREFIX}{counter}");
*counter += 1;
let sel_carrier = {
let hb = blocks.iter_mut().find(|b| b.name == header)?;
let sel_carrier = hb.typed.as_ref()?.terminator_only_block(&sel);
if let Some(t) = &mut hb.typed {
t.set_unconditional_branch(&sel);
}
sel_carrier
};
for arm in [&t, &f] {
if let Some(ab) = blocks.iter_mut().find(|b| &b.name == arm) {
if let Some(t) = &mut ab.typed {
t.rewrite_phi_predecessor(header, &sel);
}
}
}
let at = blocks
.iter()
.position(|b| b.name == header)
.map(|i| i + 1)
.unwrap_or(blocks.len());
blocks.insert(
at,
BodyBlock {
name: sel.clone(),
role: role_for_name(&sel),
typed: Some(sel_carrier),
},
);
Some(sel)
}
pub(in crate::native) fn split_loop_header_switch(
blocks: &mut Vec<BodyBlock>,
header: &str,
merge: &str,
continue_target: &str,
counter: &mut usize,
) -> Option<String> {
let targets = {
let hb = blocks.iter().find(|b| b.name == header)?;
let is_switch = is_switch_block(hb);
if !is_switch {
return None;
}
block_successors(hb)
};
let roles = [merge, continue_target, header];
if targets.iter().any(|t| roles.contains(&t.as_str())) {
return None;
}
let sel = format!("%metal2vulkan.lhsw.{counter}");
*counter += 1;
let sel_carrier = {
let hb = blocks.iter_mut().find(|b| b.name == header)?;
let sel_carrier = hb.typed.as_ref()?.terminator_only_block(&sel);
if let Some(t) = &mut hb.typed {
t.set_unconditional_branch(&sel);
}
sel_carrier
};
for arm in &targets {
if let Some(ab) = blocks.iter_mut().find(|b| &b.name == arm) {
if let Some(t) = &mut ab.typed {
t.rewrite_phi_predecessor(header, &sel);
}
}
}
let at = blocks
.iter()
.position(|b| b.name == header)
.map(|i| i + 1)
.unwrap_or(blocks.len());
blocks.insert(
at,
BodyBlock {
name: sel.clone(),
role: role_for_name(&sel),
typed: Some(sel_carrier),
},
);
Some(sel)
}
pub(in crate::native) fn loop_break_continue_merge(
forest: &LoopForest,
loop_merges: &HashMap<String, LoopMergeInfo>,
b: &str,
t: &str,
f: &str,
) -> Option<String> {
for l in &forest.loops {
if !l.body.iter().any(|n| n == b) {
continue;
}
let Some(info) = loop_merges.get(&l.header) else {
continue;
};
let arms = [t, f];
if !arms.contains(&info.merge.as_str()) {
continue;
}
let other = if t == info.merge { f } else { t };
if other == info.continue_target
&& info.merge != info.continue_target
&& info.continue_target != b
{
return Some(info.continue_target.clone());
}
}
None
}
pub(in crate::native) fn bare_loop_exit_branch(
forest: &LoopForest,
loop_merges: &HashMap<String, LoopMergeInfo>,
b: &str,
t: &str,
f: &str,
) -> bool {
for l in &forest.loops {
if l.header == b || !l.body.iter().any(|n| n == b) {
continue;
}
let Some(info) = loop_merges.get(&l.header) else {
continue;
};
let is_role = |n: &str| n == info.merge || n == info.continue_target;
if (is_role(t) || is_role(f)) && t != l.header && f != l.header {
return true;
}
}
false
}
pub(in crate::native) fn bare_loop_exit_branch_with_passthroughs(
blocks: &[BodyBlock],
forest: &LoopForest,
loop_merges: &HashMap<String, LoopMergeInfo>,
b: &str,
t: &str,
f: &str,
) -> bool {
let by_name = blocks
.iter()
.map(|block| (block.name.as_str(), block))
.collect::<HashMap<_, _>>();
for l in &forest.loops {
if l.header == b || !l.body.iter().any(|n| n == b) {
continue;
}
let Some(info) = loop_merges.get(&l.header) else {
continue;
};
let is_role = |n: &str| loop_role_or_passthrough(&by_name, n, info);
if (is_role(t) || is_role(f)) && t != l.header && f != l.header {
return true;
}
}
false
}
pub(in crate::native) fn enclosing_selection_region_exit_target(
blocks: &[BodyBlock],
forest: &LoopForest,
selection_merges: &HashMap<String, String>,
b: &str,
t: &str,
f: &str,
merge: Option<&str>,
) -> Option<String> {
if b.starts_with(LOOP_HEADER_SELECTION_PREFIX) {
return None;
}
for target in [t, f] {
if merge.is_some_and(|merge| target == merge) {
continue;
}
if !forest.dominates(b, target) {
continue;
}
if let Some(exit) = first_enclosing_selection_region_exit(
blocks,
forest,
selection_merges,
b,
target,
merge,
) {
return Some(exit);
}
}
None
}
fn first_enclosing_selection_region_exit(
blocks: &[BodyBlock],
forest: &LoopForest,
selection_merges: &HashMap<String, String>,
b: &str,
target: &str,
merge: Option<&str>,
) -> Option<String> {
let by_name = blocks
.iter()
.map(|block| (block.name.as_str(), block))
.collect::<HashMap<_, _>>();
let mut seen = HashSet::new();
let mut stack = vec![target.to_string()];
while let Some(node) = stack.pop() {
if !seen.insert(node.clone()) {
continue;
}
if merge.is_some_and(|merge| node == merge) {
continue;
}
let Some(block) = by_name.get(node.as_str()) else {
continue;
};
for successor in block_successors(block) {
if merge.is_some_and(|merge| successor == merge) {
continue;
}
if forest.dominates(b, &successor) {
stack.push(successor);
} else if exits_to_enclosing_selection_region(
blocks,
forest,
selection_merges,
b,
&successor,
) {
return Some(successor);
}
}
}
None
}
fn dominated_region_exits(
blocks: &[BodyBlock],
forest: &LoopForest,
header: &str,
merge: Option<&str>,
) -> HashSet<String> {
blocks
.iter()
.filter(|block| forest.dominates(header, &block.name))
.filter(|block| merge.is_none_or(|merge| block.name != merge))
.flat_map(block_successors)
.filter(|successor| merge.is_none_or(|merge| successor != merge))
.filter(|successor| !forest.dominates(header, successor))
.collect()
}
pub(in crate::native) fn repair_construct_tree_enclosing_selection_region_escapes(
blocks: &mut Vec<BodyBlock>,
loop_merges: &HashMap<String, LoopMergeInfo>,
header_merges: &mut HashMap<String, String>,
counter: &mut usize,
) {
for _ in 0..64 {
let forest = analyze(blocks);
let mut headers = header_merges
.iter()
.filter_map(|(header, merge)| {
let block = blocks.iter().find(|block| block.name == *header)?;
conditional_branch_targets(block)?;
Some((header.clone(), merge.clone()))
})
.collect::<Vec<_>>();
headers.sort_by_key(|(header, _)| std::cmp::Reverse(depth_from_forest(&forest, header)));
let mut repaired = false;
for (header, merge) in headers {
let Some(exit_target) = selection_region_enclosing_escape_target(
blocks,
&forest,
loop_merges,
header_merges,
&header,
&merge,
) else {
continue;
};
let synth = if block_has_phi(blocks, &exit_target) {
synth_unique_selection_merge_phi(blocks, &forest, &header, &exit_target, counter)
} else {
synth_unique_selection_merge(blocks, &forest, &header, &exit_target, counter)
};
if let Some(private_merge) = synth {
header_merges.insert(header, private_merge);
repaired = true;
break;
}
}
if !repaired {
break;
}
}
}
fn selection_region_enclosing_escape_target(
blocks: &[BodyBlock],
forest: &LoopForest,
loop_merges: &HashMap<String, LoopMergeInfo>,
header_merges: &HashMap<String, String>,
header: &str,
merge: &str,
) -> Option<String> {
let by_name = blocks
.iter()
.map(|block| (block.name.as_str(), block))
.collect::<HashMap<_, _>>();
let header_block = by_name.get(header)?;
let mut seen = HashSet::new();
let mut stack = block_successors(header_block)
.into_iter()
.filter(|target| target != merge)
.collect::<Vec<_>>();
let mut exit_target = None;
while let Some(node) = stack.pop() {
if node == merge || !seen.insert(node.clone()) {
continue;
}
let Some(block) = by_name.get(node.as_str()) else {
continue;
};
if !forest.dominates(header, &node) {
continue;
}
for successor in block_successors(block) {
if successor == merge {
continue;
}
if legal_enclosing_loop_exit(forest, loop_merges, &node, &successor) {
continue;
}
if forest.dominates(header, &successor) {
stack.push(successor);
continue;
}
if exits_to_enclosing_selection_region(blocks, forest, header_merges, &node, &successor)
{
if exit_target
.as_ref()
.is_some_and(|target: &String| target != &successor)
{
return None;
}
exit_target = Some(successor);
}
}
}
exit_target
}
fn legal_enclosing_loop_exit(
forest: &LoopForest,
loop_merges: &HashMap<String, LoopMergeInfo>,
source: &str,
target: &str,
) -> bool {
forest.loops.iter().any(|loop_info| {
let encloses =
loop_info.header == source || loop_info.body.iter().any(|name| name == source);
encloses
&& (loop_info.header == target
|| loop_merges
.get(&loop_info.header)
.is_some_and(|info| info.merge == target || info.continue_target == target))
})
}
fn depth_from_forest(forest: &LoopForest, name: &str) -> usize {
let mut depth = 0usize;
let mut current = name;
while let Some(parent) = forest.idom(current) {
depth += 1;
current = parent;
}
depth
}
pub(in crate::native) fn bare_enclosing_selection_region_escape(
blocks: &[BodyBlock],
forest: &LoopForest,
selection_merges: &HashMap<String, String>,
source: &str,
target: &str,
) -> bool {
if forest.dominates(source, target) {
return false;
}
let by_name = blocks
.iter()
.map(|block| (block.name.as_str(), block))
.collect::<HashMap<_, _>>();
let mut current = Some(source);
while let Some(candidate) = current {
if let Some(block) = by_name.get(candidate) {
if !selection_merges.contains_key(candidate)
&& conditional_branch_targets(block).is_some()
&& !dominated_region_exits(blocks, forest, candidate, None).is_empty()
&& forest.dominates(candidate, source)
&& !forest.dominates(candidate, target)
&& exits_to_enclosing_selection_region(
blocks,
forest,
selection_merges,
candidate,
target,
)
{
return true;
}
}
current = forest.idom(candidate);
}
false
}
fn exits_to_enclosing_selection_region(
blocks: &[BodyBlock],
forest: &LoopForest,
selection_merges: &HashMap<String, String>,
b: &str,
target: &str,
) -> bool {
let by_name = blocks
.iter()
.map(|block| (block.name.as_str(), block))
.collect::<HashMap<_, _>>();
let mut current = b;
while let Some(parent) = forest.idom(current) {
if let Some(merge) = selection_merges.get(parent) {
if let Some(header) = by_name.get(parent) {
if let Some((left, right)) = conditional_branch_targets(header) {
let in_left = forest.dominates(&left, b);
let in_right = forest.dominates(&right, b);
if in_left != in_right && (target == merge || forest.dominates(parent, target))
{
return true;
}
}
}
}
current = parent;
}
false
}
fn loop_role_or_passthrough(
by_name: &HashMap<&str, &BodyBlock>,
start: &str,
info: &LoopMergeInfo,
) -> bool {
let mut current = start.to_string();
for _ in 0..=by_name.len() {
if current == info.merge || current == info.continue_target {
return true;
}
let Some(block) = by_name.get(current.as_str()) else {
return false;
};
if !matches!(
block.role,
BlockRole::LMerge | BlockRole::ConstructTreeRoute
) {
return false;
}
let successors = block_successors(block);
if successors.len() != 1 || successors[0] == current {
return false;
}
current = successors[0].clone();
}
false
}
pub(in crate::native) fn bare_exit_escape_reason(
ordered: &[BodyBlock],
header_merge: &HashMap<String, String>,
loop_merges: &HashMap<String, LoopMergeInfo>,
) -> Option<&'static str> {
let forest = analyze(ordered);
let names: HashSet<&str> = ordered.iter().map(|b| b.name.as_str()).collect();
let switch_targets: HashMap<&str, HashSet<String>> = ordered
.iter()
.filter(|b| is_switch_block(b))
.map(|b| (b.name.as_str(), block_successors(b).into_iter().collect()))
.collect();
for b in ordered {
for s in block_successors(b) {
if s == b.name || !names.contains(s.as_str()) {
continue;
}
if forest.dominates(&b.name, &s) || header_merge.get(&b.name).is_some_and(|m| m == &s) {
continue;
}
let loop_ok = forest.loops.iter().any(|l| {
let encloses = l.header == b.name || l.body.iter().any(|n| n == &b.name);
encloses
&& (l.header == s
|| loop_merges
.get(&l.header)
.is_some_and(|i| i.merge == s || i.continue_target == s))
});
let sel_break = header_merge
.iter()
.any(|(h, m)| m == &s && h != &b.name && forest.dominates(h, &b.name));
let case_ok = switch_targets.iter().any(|(h, tg)| {
*h != b.name.as_str() && forest.dominates(h, &b.name) && tg.contains(&s)
});
let enclosing_selection_region_ok =
bare_enclosing_selection_region_escape(ordered, &forest, header_merge, &b.name, &s);
if loop_ok || sel_break || case_ok || enclosing_selection_region_ok {
continue;
}
return Some("bare-exit:unstructured-escape");
}
}
None
}