Auto merge of #77372 - jonas-schievink:rollup-e5bdzga, r=jonas-schievink

Rollup of 12 pull requests

Successful merges:

 - #77037 (more tiny clippy cleanups)
 - #77233 (BTreeMap: keep an eye out on the size of the main components)
 - #77280 (Ensure that all LLVM components requested by tests are available on CI)
 - #77284 (library: Forward compiler-builtins "mem" feature)
 - #77296 (liveness: Use Option::None to represent absent live nodes)
 - #77322 (Add unstable book docs for `-Zunsound-mir-opts`)
 - #77328 (Use `rtassert!` instead of `assert!` from the child process after fork() in std::sys::unix::process::Command::spawn())
 - #77331 (Add test for async/await combined with const-generics.)
 - #77338 (Fix typo in alloc vec comment)
 - #77340 (Alloc vec use imported path)
 - #77345 (Add test for issue #74761)
 - #77348 (Update books)

Failed merges:

r? `@ghost`
This commit is contained in:
bors 2020-09-30 20:49:27 +00:00
commit ef663a8a48
17 changed files with 127 additions and 54 deletions

View File

@ -62,8 +62,7 @@ impl<'a, 'tcx> ConstraintConversion<'a, 'tcx> {
// `self.constraints`, but we also want to be mutating // `self.constraints`, but we also want to be mutating
// `self.member_constraints`. For now, just swap out the value // `self.member_constraints`. For now, just swap out the value
// we want and replace at the end. // we want and replace at the end.
let mut tmp = let mut tmp = std::mem::take(&mut self.constraints.member_constraints);
std::mem::replace(&mut self.constraints.member_constraints, Default::default());
for member_constraint in member_constraints { for member_constraint in member_constraints {
tmp.push_constraint(member_constraint, |r| self.to_region_vid(r)); tmp.push_constraint(member_constraint, |r| self.to_region_vid(r));
} }

View File

@ -91,7 +91,7 @@ impl<'tcx> MirPass<'tcx> for EarlyOtherwiseBranch {
let not_equal_rvalue = Rvalue::BinaryOp( let not_equal_rvalue = Rvalue::BinaryOp(
not_equal, not_equal,
Operand::Copy(Place::from(second_discriminant_temp)), Operand::Copy(Place::from(second_discriminant_temp)),
Operand::Copy(Place::from(first_descriminant_place)), Operand::Copy(first_descriminant_place),
); );
patch.add_statement( patch.add_statement(
end_of_block_location, end_of_block_location,

View File

@ -62,13 +62,13 @@
//! - `reader`: the `LiveNode` ID of some node which will read the value //! - `reader`: the `LiveNode` ID of some node which will read the value
//! that `V` holds on entry to `N`. Formally: a node `M` such //! that `V` holds on entry to `N`. Formally: a node `M` such
//! that there exists a path `P` from `N` to `M` where `P` does not //! that there exists a path `P` from `N` to `M` where `P` does not
//! write `V`. If the `reader` is `INVALID_NODE`, then the current //! write `V`. If the `reader` is `None`, then the current
//! value will never be read (the variable is dead, essentially). //! value will never be read (the variable is dead, essentially).
//! //!
//! - `writer`: the `LiveNode` ID of some node which will write the //! - `writer`: the `LiveNode` ID of some node which will write the
//! variable `V` and which is reachable from `N`. Formally: a node `M` //! variable `V` and which is reachable from `N`. Formally: a node `M`
//! such that there exists a path `P` from `N` to `M` and `M` writes //! such that there exists a path `P` from `N` to `M` and `M` writes
//! `V`. If the `writer` is `INVALID_NODE`, then there is no writer //! `V`. If the `writer` is `None`, then there is no writer
//! of `V` that follows `N`. //! of `V` that follows `N`.
//! //!
//! - `used`: a boolean value indicating whether `V` is *used*. We //! - `used`: a boolean value indicating whether `V` is *used*. We
@ -114,7 +114,6 @@ rustc_index::newtype_index! {
rustc_index::newtype_index! { rustc_index::newtype_index! {
pub struct LiveNode { pub struct LiveNode {
DEBUG_FORMAT = "ln({})", DEBUG_FORMAT = "ln({})",
const INVALID_NODE = LiveNode::MAX_AS_U32,
} }
} }
@ -168,12 +167,6 @@ pub fn provide(providers: &mut Providers) {
// variable must not be assigned if there is some successor // variable must not be assigned if there is some successor
// assignment. And so forth. // assignment. And so forth.
impl LiveNode {
fn is_valid(self) -> bool {
self != INVALID_NODE
}
}
struct CaptureInfo { struct CaptureInfo {
ln: LiveNode, ln: LiveNode,
var_hid: HirId, var_hid: HirId,
@ -467,8 +460,8 @@ impl<'tcx> Visitor<'tcx> for IrMaps<'tcx> {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct RWU { struct RWU {
reader: LiveNode, reader: Option<LiveNode>,
writer: LiveNode, writer: Option<LiveNode>,
used: bool, used: bool,
} }
@ -490,10 +483,10 @@ struct RWUTable {
unpacked_rwus: Vec<RWU>, unpacked_rwus: Vec<RWU>,
} }
// A constant representing `RWU { reader: INVALID_NODE; writer: INVALID_NODE; used: false }`. // A constant representing `RWU { reader: None; writer: None; used: false }`.
const INV_INV_FALSE: u32 = u32::MAX; const INV_INV_FALSE: u32 = u32::MAX;
// A constant representing `RWU { reader: INVALID_NODE; writer: INVALID_NODE; used: true }`. // A constant representing `RWU { reader: None; writer: None; used: true }`.
const INV_INV_TRUE: u32 = u32::MAX - 1; const INV_INV_TRUE: u32 = u32::MAX - 1;
impl RWUTable { impl RWUTable {
@ -504,24 +497,24 @@ impl RWUTable {
fn get(&self, idx: usize) -> RWU { fn get(&self, idx: usize) -> RWU {
let packed_rwu = self.packed_rwus[idx]; let packed_rwu = self.packed_rwus[idx];
match packed_rwu { match packed_rwu {
INV_INV_FALSE => RWU { reader: INVALID_NODE, writer: INVALID_NODE, used: false }, INV_INV_FALSE => RWU { reader: None, writer: None, used: false },
INV_INV_TRUE => RWU { reader: INVALID_NODE, writer: INVALID_NODE, used: true }, INV_INV_TRUE => RWU { reader: None, writer: None, used: true },
_ => self.unpacked_rwus[packed_rwu as usize], _ => self.unpacked_rwus[packed_rwu as usize],
} }
} }
fn get_reader(&self, idx: usize) -> LiveNode { fn get_reader(&self, idx: usize) -> Option<LiveNode> {
let packed_rwu = self.packed_rwus[idx]; let packed_rwu = self.packed_rwus[idx];
match packed_rwu { match packed_rwu {
INV_INV_FALSE | INV_INV_TRUE => INVALID_NODE, INV_INV_FALSE | INV_INV_TRUE => None,
_ => self.unpacked_rwus[packed_rwu as usize].reader, _ => self.unpacked_rwus[packed_rwu as usize].reader,
} }
} }
fn get_writer(&self, idx: usize) -> LiveNode { fn get_writer(&self, idx: usize) -> Option<LiveNode> {
let packed_rwu = self.packed_rwus[idx]; let packed_rwu = self.packed_rwus[idx];
match packed_rwu { match packed_rwu {
INV_INV_FALSE | INV_INV_TRUE => INVALID_NODE, INV_INV_FALSE | INV_INV_TRUE => None,
_ => self.unpacked_rwus[packed_rwu as usize].writer, _ => self.unpacked_rwus[packed_rwu as usize].writer,
} }
} }
@ -541,7 +534,7 @@ impl RWUTable {
} }
fn assign_unpacked(&mut self, idx: usize, rwu: RWU) { fn assign_unpacked(&mut self, idx: usize, rwu: RWU) {
if rwu.reader == INVALID_NODE && rwu.writer == INVALID_NODE { if rwu.reader == None && rwu.writer == None {
// When we overwrite an indexing entry in `self.packed_rwus` with // When we overwrite an indexing entry in `self.packed_rwus` with
// `INV_INV_{TRUE,FALSE}` we don't remove the corresponding entry // `INV_INV_{TRUE,FALSE}` we don't remove the corresponding entry
// from `self.unpacked_rwus`; it's not worth the effort, and we // from `self.unpacked_rwus`; it's not worth the effort, and we
@ -570,7 +563,7 @@ struct Liveness<'a, 'tcx> {
typeck_results: &'a ty::TypeckResults<'tcx>, typeck_results: &'a ty::TypeckResults<'tcx>,
param_env: ty::ParamEnv<'tcx>, param_env: ty::ParamEnv<'tcx>,
upvars: Option<&'tcx FxIndexMap<hir::HirId, hir::Upvar>>, upvars: Option<&'tcx FxIndexMap<hir::HirId, hir::Upvar>>,
successors: IndexVec<LiveNode, LiveNode>, successors: IndexVec<LiveNode, Option<LiveNode>>,
rwu_table: RWUTable, rwu_table: RWUTable,
/// A live node representing a point of execution before closure entry & /// A live node representing a point of execution before closure entry &
@ -606,7 +599,7 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
typeck_results, typeck_results,
param_env, param_env,
upvars, upvars,
successors: IndexVec::from_elem_n(INVALID_NODE, num_live_nodes), successors: IndexVec::from_elem_n(None, num_live_nodes),
rwu_table: RWUTable::new(num_live_nodes * num_vars), rwu_table: RWUTable::new(num_live_nodes * num_vars),
closure_ln, closure_ln,
exit_ln, exit_ln,
@ -651,30 +644,33 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
} }
fn live_on_entry(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> { fn live_on_entry(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> {
assert!(ln.is_valid()); if let Some(reader) = self.rwu_table.get_reader(self.idx(ln, var)) {
let reader = self.rwu_table.get_reader(self.idx(ln, var)); Some(self.ir.lnks[reader])
if reader.is_valid() { Some(self.ir.lnks[reader]) } else { None } } else {
None
}
} }
// Is this variable live on entry to any of its successor nodes? // Is this variable live on entry to any of its successor nodes?
fn live_on_exit(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> { fn live_on_exit(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> {
let successor = self.successors[ln]; let successor = self.successors[ln].unwrap();
self.live_on_entry(successor, var) self.live_on_entry(successor, var)
} }
fn used_on_entry(&self, ln: LiveNode, var: Variable) -> bool { fn used_on_entry(&self, ln: LiveNode, var: Variable) -> bool {
assert!(ln.is_valid());
self.rwu_table.get_used(self.idx(ln, var)) self.rwu_table.get_used(self.idx(ln, var))
} }
fn assigned_on_entry(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> { fn assigned_on_entry(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> {
assert!(ln.is_valid()); if let Some(writer) = self.rwu_table.get_writer(self.idx(ln, var)) {
let writer = self.rwu_table.get_writer(self.idx(ln, var)); Some(self.ir.lnks[writer])
if writer.is_valid() { Some(self.ir.lnks[writer]) } else { None } } else {
None
}
} }
fn assigned_on_exit(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> { fn assigned_on_exit(&self, ln: LiveNode, var: Variable) -> Option<LiveNodeKind> {
let successor = self.successors[ln]; let successor = self.successors[ln].unwrap();
self.assigned_on_entry(successor, var) self.assigned_on_entry(successor, var)
} }
@ -709,9 +705,9 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
{ {
let wr = &mut wr as &mut dyn Write; let wr = &mut wr as &mut dyn Write;
write!(wr, "[{:?} of kind {:?} reads", ln, self.ir.lnks[ln]); write!(wr, "[{:?} of kind {:?} reads", ln, self.ir.lnks[ln]);
self.write_vars(wr, ln, |idx| self.rwu_table.get_reader(idx).is_valid()); self.write_vars(wr, ln, |idx| self.rwu_table.get_reader(idx).is_some());
write!(wr, " writes"); write!(wr, " writes");
self.write_vars(wr, ln, |idx| self.rwu_table.get_writer(idx).is_valid()); self.write_vars(wr, ln, |idx| self.rwu_table.get_writer(idx).is_some());
write!(wr, " uses"); write!(wr, " uses");
self.write_vars(wr, ln, |idx| self.rwu_table.get_used(idx)); self.write_vars(wr, ln, |idx| self.rwu_table.get_used(idx));
@ -735,7 +731,7 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
} }
fn init_empty(&mut self, ln: LiveNode, succ_ln: LiveNode) { fn init_empty(&mut self, ln: LiveNode, succ_ln: LiveNode) {
self.successors[ln] = succ_ln; self.successors[ln] = Some(succ_ln);
// It is not necessary to initialize the RWUs here because they are all // It is not necessary to initialize the RWUs here because they are all
// set to INV_INV_FALSE when they are created, and the sets only grow // set to INV_INV_FALSE when they are created, and the sets only grow
@ -744,7 +740,7 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
fn init_from_succ(&mut self, ln: LiveNode, succ_ln: LiveNode) { fn init_from_succ(&mut self, ln: LiveNode, succ_ln: LiveNode) {
// more efficient version of init_empty() / merge_from_succ() // more efficient version of init_empty() / merge_from_succ()
self.successors[ln] = succ_ln; self.successors[ln] = Some(succ_ln);
self.indices2(ln, succ_ln, |this, idx, succ_idx| { self.indices2(ln, succ_ln, |this, idx, succ_idx| {
this.rwu_table.copy_packed(idx, succ_idx); this.rwu_table.copy_packed(idx, succ_idx);
@ -768,12 +764,12 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
let mut changed = false; let mut changed = false;
let mut rwu = this.rwu_table.get(idx); let mut rwu = this.rwu_table.get(idx);
let succ_rwu = this.rwu_table.get(succ_idx); let succ_rwu = this.rwu_table.get(succ_idx);
if succ_rwu.reader.is_valid() && !rwu.reader.is_valid() { if succ_rwu.reader.is_some() && rwu.reader.is_none() {
rwu.reader = succ_rwu.reader; rwu.reader = succ_rwu.reader;
changed = true changed = true
} }
if succ_rwu.writer.is_valid() && !rwu.writer.is_valid() { if succ_rwu.writer.is_some() && rwu.writer.is_none() {
rwu.writer = succ_rwu.writer; rwu.writer = succ_rwu.writer;
changed = true changed = true
} }
@ -817,14 +813,14 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
let mut rwu = self.rwu_table.get(idx); let mut rwu = self.rwu_table.get(idx);
if (acc & ACC_WRITE) != 0 { if (acc & ACC_WRITE) != 0 {
rwu.reader = INVALID_NODE; rwu.reader = None;
rwu.writer = ln; rwu.writer = Some(ln);
} }
// Important: if we both read/write, must do read second // Important: if we both read/write, must do read second
// or else the write will override. // or else the write will override.
if (acc & ACC_READ) != 0 { if (acc & ACC_READ) != 0 {
rwu.reader = ln; rwu.reader = Some(ln);
} }
if (acc & ACC_USE) != 0 { if (acc & ACC_USE) != 0 {

View File

@ -70,10 +70,8 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
debug!("used_trait_imports({:?}) = {:?}", item_def_id, used_trait_imports); debug!("used_trait_imports({:?}) = {:?}", item_def_id, used_trait_imports);
wbcx.typeck_results.used_trait_imports = used_trait_imports; wbcx.typeck_results.used_trait_imports = used_trait_imports;
wbcx.typeck_results.closure_captures = mem::replace( wbcx.typeck_results.closure_captures =
&mut self.typeck_results.borrow_mut().closure_captures, mem::take(&mut self.typeck_results.borrow_mut().closure_captures);
Default::default(),
);
if self.is_tainted_by_errors() { if self.is_tainted_by_errors() {
// FIXME(eddyb) keep track of `ErrorReported` from where the error was emitted. // FIXME(eddyb) keep track of `ErrorReported` from where the error was emitted.

View File

@ -23,3 +23,12 @@ fn test_splitpoint() {
assert!(left_len + right_len == CAPACITY); assert!(left_len + right_len == CAPACITY);
} }
} }
#[test]
#[cfg(target_arch = "x86_64")]
fn test_sizes() {
assert_eq!(core::mem::size_of::<LeafNode<(), ()>>(), 16);
assert_eq!(core::mem::size_of::<LeafNode<i64, i64>>(), 16 + CAPACITY * 8 * 2);
assert_eq!(core::mem::size_of::<InternalNode<(), ()>>(), 112);
assert_eq!(core::mem::size_of::<InternalNode<i64, i64>>(), 112 + CAPACITY * 8 * 2);
}

View File

@ -2281,14 +2281,14 @@ where
// use try-fold since // use try-fold since
// - it vectorizes better for some iterator adapters // - it vectorizes better for some iterator adapters
// - unlike most internal iteration methods methods it only takes a &mut self // - unlike most internal iteration methods, it only takes a &mut self
// - it lets us thread the write pointer through its innards and get it back in the end // - it lets us thread the write pointer through its innards and get it back in the end
let sink = InPlaceDrop { inner: dst_buf, dst: dst_buf }; let sink = InPlaceDrop { inner: dst_buf, dst: dst_buf };
let sink = iterator let sink = iterator
.try_fold::<_, _, Result<_, !>>(sink, write_in_place_with_drop(dst_end)) .try_fold::<_, _, Result<_, !>>(sink, write_in_place_with_drop(dst_end))
.unwrap(); .unwrap();
// iteration succeeded, don't drop head // iteration succeeded, don't drop head
let dst = mem::ManuallyDrop::new(sink).dst; let dst = ManuallyDrop::new(sink).dst;
let src = unsafe { iterator.as_inner().as_into_iter() }; let src = unsafe { iterator.as_inner().as_into_iter() };
// check if SourceIter contract was upheld // check if SourceIter contract was upheld

View File

@ -59,6 +59,7 @@ gimli-symbolize = []
panic-unwind = ["panic_unwind"] panic-unwind = ["panic_unwind"]
profiler = ["profiler_builtins"] profiler = ["profiler_builtins"]
compiler-builtins-c = ["alloc/compiler-builtins-c"] compiler-builtins-c = ["alloc/compiler-builtins-c"]
compiler-builtins-mem = ["alloc/compiler-builtins-mem"]
llvm-libunwind = ["unwind/llvm-libunwind"] llvm-libunwind = ["unwind/llvm-libunwind"]
# Make panics and failed asserts immediately abort without formatting any message # Make panics and failed asserts immediately abort without formatting any message

View File

@ -67,7 +67,7 @@ impl Command {
// pipe I/O up to PIPE_BUF bytes should be atomic, and then // pipe I/O up to PIPE_BUF bytes should be atomic, and then
// we want to be sure we *don't* run at_exit destructors as // we want to be sure we *don't* run at_exit destructors as
// we're being torn down regardless // we're being torn down regardless
assert!(output.write(&bytes).is_ok()); rtassert!(output.write(&bytes).is_ok());
libc::_exit(1) libc::_exit(1)
} }
n => n, n => n,

View File

@ -25,6 +25,7 @@ proc_macro = { path = "../proc_macro" }
default = ["std_detect_file_io", "std_detect_dlsym_getauxval", "panic-unwind"] default = ["std_detect_file_io", "std_detect_dlsym_getauxval", "panic-unwind"]
backtrace = ["std/backtrace"] backtrace = ["std/backtrace"]
compiler-builtins-c = ["std/compiler-builtins-c"] compiler-builtins-c = ["std/compiler-builtins-c"]
compiler-builtins-mem = ["std/compiler-builtins-mem"]
llvm-libunwind = ["std/llvm-libunwind"] llvm-libunwind = ["std/llvm-libunwind"]
panic-unwind = ["std/panic_unwind"] panic-unwind = ["std/panic_unwind"]
panic_immediate_abort = ["std/panic_immediate_abort"] panic_immediate_abort = ["std/panic_immediate_abort"]

View File

@ -104,6 +104,8 @@ if [ "$RUST_RELEASE_CHANNEL" = "nightly" ] || [ "$DIST_REQUIRE_ALL_TOOLS" = "" ]
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --enable-missing-tools" RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --enable-missing-tools"
fi fi
export COMPILETEST_NEEDS_ALL_LLVM_COMPONENTS=1
# Print the date from the local machine and the date from an external source to # Print the date from the local machine and the date from an external source to
# check for clock drifts. An HTTP URL is used instead of HTTPS since on Azure # check for clock drifts. An HTTP URL is used instead of HTTPS since on Azure
# Pipelines it happened that the certificates were marked as expired. # Pipelines it happened that the certificates were marked as expired.

@ -1 +1 @@
Subproject commit 0cd2ca116274b915924c3a7e07c1e046b6f19b77 Subproject commit dd310616308e01f6cf227f46347b744aa56b77d9

@ -1 +1 @@
Subproject commit 19f0a0372af497b34369cf182d9d16156cab2969 Subproject commit 7d3ff1c12db08a847a57a054be4a7951ce532d2d

View File

@ -0,0 +1,8 @@
# `unsound-mir-opts`
--------------------
The `-Zunsound-mir-opts` compiler flag enables [MIR optimization passes] which can cause unsound behavior.
This flag should only be used by MIR optimization tests in the rustc test suite.
[MIR optimization passes]: https://rustc-dev-guide.rust-lang.org/mir/optimizations.html

View File

@ -0,0 +1,25 @@
// edition:2018
// check-pass
// revisions: full min
#![cfg_attr(full, feature(const_generics))]
#![cfg_attr(full, allow(incomplete_features))]
#![cfg_attr(min, feature(min_const_generics))]
const SIZE: usize = 16;
struct Bar<const H: usize> {}
struct Foo<const H: usize> {}
impl<const H: usize> Foo<H> {
async fn biz(_: &[[u8; SIZE]]) -> Vec<()> {
vec![]
}
pub async fn baz(&self) -> Bar<H> {
Self::biz(&vec![]).await;
Bar {}
}
}
fn main() { }

View File

@ -0,0 +1,16 @@
#![feature(member_constraints)]
#![feature(type_alias_impl_trait)]
pub trait A {
type B;
fn f(&self) -> Self::B;
}
impl<'a, 'b> A for () {
//~^ ERROR the lifetime parameter `'a` is not constrained
//~| ERROR the lifetime parameter `'b` is not constrained
type B = impl core::fmt::Debug;
fn f(&self) -> Self::B {}
}
fn main() {}

View File

@ -0,0 +1,15 @@
error[E0207]: the lifetime parameter `'a` is not constrained by the impl trait, self type, or predicates
--> $DIR/issue-74761.rs:8:6
|
LL | impl<'a, 'b> A for () {
| ^^ unconstrained lifetime parameter
error[E0207]: the lifetime parameter `'b` is not constrained by the impl trait, self type, or predicates
--> $DIR/issue-74761.rs:8:10
|
LL | impl<'a, 'b> A for () {
| ^^ unconstrained lifetime parameter
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0207`.

View File

@ -208,10 +208,13 @@ impl EarlyProps {
config.parse_name_value_directive(line, "needs-llvm-components") config.parse_name_value_directive(line, "needs-llvm-components")
{ {
let components: HashSet<_> = config.llvm_components.split_whitespace().collect(); let components: HashSet<_> = config.llvm_components.split_whitespace().collect();
if !needed_components if let Some(missing_component) = needed_components
.split_whitespace() .split_whitespace()
.all(|needed_component| components.contains(needed_component)) .find(|needed_component| !components.contains(needed_component))
{ {
if env::var_os("COMPILETEST_NEEDS_ALL_LLVM_COMPONENTS").is_some() {
panic!("missing LLVM component: {}", missing_component);
}
return true; return true;
} }
} }