diff --git a/Cargo.toml b/Cargo.toml index e3a6fb1..07c55a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,6 @@ chan_select = ["compiletest_rs"] [dependencies] compiletest_rs = { version = "*", optional = true } + +[dependencies.branch_impls] +path = "src/branch_impls" \ No newline at end of file diff --git a/examples/arithmetic.rs b/examples/arithmetic.rs index 4e9bb19..60ce1f7 100644 --- a/examples/arithmetic.rs +++ b/examples/arithmetic.rs @@ -9,30 +9,31 @@ use std::thread::spawn; // Offers: Add, Negate, Sqrt, Eval type Srv = - Offer>>>, - Offer>>, - Offer>, Var>>, - Recv bool, Recv>>>>>>>; + Offer<(Eps, + Recv>>>, + Recv>>, + Recv>, Var)>>, + Recv bool, Recv>>>)>; fn server(c: Chan<(), Rec>) { let mut c = c.enter(); loop { - c = offer!{ c, - CLOSE => { + use session_types::Branch5::*; + c = match c.offer() { + B1(c) => { c.close(); return }, - ADD => { + B2(c) => { let (c, n) = c.recv(); let (c, m) = c.recv(); c.send(n + m).zero() }, - NEGATE => { + B3(c) => { let (c, n) = c.recv(); c.send(-n).zero() }, - SQRT => { + B4(c) => { let (c, x) = c.recv(); if x >= 0.0 { c.sel1().send(x.sqrt()).zero() @@ -40,7 +41,7 @@ fn server(c: Chan<(), Rec>) { c.sel2().zero() } }, - EVAL => { + B5(c) => { let (c, f) = c.recv(); let (c, n) = c.recv(); c.send(f(n)).zero() @@ -53,43 +54,44 @@ fn server(c: Chan<(), Rec>) { // uses of session types, but they do showcase subtyping, recursion and how to // work the types in general. -type AddCli = - Choose>>>, R>>; +type AddCli = + Choose<(Eps, + Send>>>, + R, S, T)>; -fn add_client(c: Chan<(), Rec>>) { - let (c, n) = c.enter().sel2().sel1().send(42).send(1).recv(); +fn add_client(c: Chan<(), Rec>>) { + let (c, n) = c.enter().sel2().send(42).send(1).recv(); println!("{}", n); c.zero().sel1().close() } -type NegCli = - Choose>>, - S>>>; +type NegCli = + Choose<(Eps, + R, + Send>>, + S, T)>; -fn neg_client(c: Chan<(), Rec>>) { - let (c, n) = c.enter().skip2().sel1().send(42).recv(); +fn neg_client(c: Chan<(), Rec>>) { + let (c, n) = c.enter().sel3().send(42).recv(); println!("{}", n); c.zero().sel1().close(); } type SqrtCli = - Choose>, Var>>, - T>>>>; + Choose<(Eps, + R, + S, + Send>, Var)>>, + T)>; fn sqrt_client(c: Chan<(), Rec>>) { - match c.enter().skip3().sel1().send(42.0).offer() { - Left(c) => { + match c.enter().sel4().send(42.0).offer() { + B1(c) => { let (c, n) = c.recv(); println!("{}", n); c.zero().sel1().close(); } - Right(c) => { + B2(c) => { println!("Couldn't take square root!"); c.zero().sel1().close(); } @@ -99,11 +101,11 @@ fn sqrt_client(c: Chan<(), Rec>>) { // `fn_client` sends a function over the channel type PrimeCli = - Choose bool, Send>>>>>>>; + Choose<(Eps, + R, + S, + T, + Send bool, Send>>>)>; fn fn_client(c: Chan<(), Rec>>) { fn even(n: i64) -> bool { @@ -111,7 +113,7 @@ fn fn_client(c: Chan<(), Rec>>) { } let (c, b) = c.enter() - .skip4() + .sel5() .send(even) .send(42) .recv(); @@ -119,7 +121,6 @@ fn fn_client(c: Chan<(), Rec>>) { c.zero().sel1().close(); } - // `ask_neg` and `get_neg` use delegation, that is, sending a channel over // another channel. @@ -127,20 +128,22 @@ fn fn_client(c: Chan<(), Rec>>) { // sends the whole channel to `get_neg`. `get_neg` then receives the negated // integer and prints it. -type AskNeg = - Choose>>, - S>>>; +type AskNeg = + Choose<(Eps, + R, + Send>>, + S, T)>; -fn ask_neg(c1: Chan<(), Rec>>, - c2: Chan<(), Send, ()), Recv>>, Eps>>) { - let c1 = c1.enter().sel2().sel2().sel1().send(42); +fn ask_neg + (c1: Chan<(), Rec>>, + c2: Chan<(), Send, ()), Recv>>, Eps>>) { + let c1 = c1.enter().sel3().send(42); c2.send(c1).close(); } -fn get_neg(c1: Chan<(), Recv, ()), Recv>>, Eps>>) { +fn get_neg + (c1: Chan<(), Recv, ()), Recv>>, Eps>>) { let (c1, c2) = c1.recv(); let (c2, n) = c2.recv(); println!("{}", n); diff --git a/examples/atm.rs b/examples/atm.rs index 2c09bb5..f62be83 100644 --- a/examples/atm.rs +++ b/examples/atm.rs @@ -3,16 +3,14 @@ use session_types::*; use std::thread::spawn; type Id = String; -type Atm = Recv, Eps>>; +type Atm = Recv, Eps)>>; -type AtmInner = Offer>>; +type AtmInner = Offer<(AtmDeposit, + AtmWithdraw, + Eps)>; type AtmDeposit = Recv>>; -type AtmWithdraw = Recv, Var>>; -type AtmBalance = Send>; +type AtmWithdraw = Recv, Var)>>; type Client = ::Dual; @@ -30,59 +28,53 @@ fn atm(c: Chan<(), Atm>) { c.sel1().enter() }; let mut balance = 0; + loop { - c = offer! { - c, - Deposit => { + c = match c.offer() { + Branch3::B1(c) => { let (c, amt) = c.recv(); - balance += amt; - c.send(balance).zero() + balance = amt; + c.send(balance).zero() // c.send(new_bal): Chan<(AtmInner, ()) Var> }, - Withdraw => { + Branch3::B2(c) => { let (c, amt) = c.recv(); - if amt > balance { - c.sel2().zero() - } else { - balance -= amt; + if amt <= balance { + balance = balance - amt; c.sel1().zero() + } else { + c.sel2().zero() } }, - Balance => { - c.send(balance).zero() - }, - Quit => { - c.close(); - break - } - } + Branch3::B3(c) => { c.close(); break } + }; } } fn deposit_client(c: Chan<(), Client>) { let c = match c.send("Deposit Client".to_string()).offer() { - Left(c) => c.enter(), - Right(_) => panic!("deposit_client: expected to be approved") + B1(c) => c.enter(), + B2(_) => panic!("deposit_client: expected to be approved") }; let (c, new_balance) = c.sel1().send(200).recv(); println!("deposit_client: new balance: {}", new_balance); - c.zero().skip3().close(); + c.zero().sel3().close(); } fn withdraw_client(c: Chan<(), Client>) { let c = match c.send("Withdraw Client".to_string()).offer() { - Left(c) => c.enter(), - Right(_) => panic!("withdraw_client: expected to be approved") + B1(c) => c.enter(), + B2(_) => panic!("withdraw_client: expected to be approved") }; - match c.sel2().sel1().send(100).offer() { - Left(c) => { + match c.sel2().send(100).offer() { + B1(c) => { println!("withdraw_client: Successfully withdrew 100"); - c.zero().skip3().close(); + c.zero().sel3().close(); } - Right(c) => { + B2(c) => { println!("withdraw_client: Could not withdraw. Depositing instead."); - c.zero().sel1().send(50).recv().0.zero().skip3().close(); + c.zero().sel1().send(50).recv().0.zero().sel3().close(); } } } diff --git a/examples/echo-server.rs b/examples/echo-server.rs index f4a4185..af3f1c1 100644 --- a/examples/echo-server.rs +++ b/examples/echo-server.rs @@ -8,19 +8,19 @@ use session_types::*; use std::thread::spawn; -type Srv = Offer>>; +type Srv = Offer<(Eps, Recv>)>; fn srv(c: Chan<(), Rec>) { let mut c = c.enter(); loop { - c = offer!{ c, - CLOSE => { + c = match c.offer() { + Branch2::B1(c) => { println!("Closing server."); c.close(); break }, - RECV => { + Branch2::B2(c) => { let (c, s) = c.recv(); println!("Received: {}", s); c.zero() diff --git a/examples/many-clients.rs b/examples/many-clients.rs index f8c79c6..a64c5ff 100644 --- a/examples/many-clients.rs +++ b/examples/many-clients.rs @@ -6,7 +6,7 @@ use std::sync::mpsc::{channel, Receiver}; use std::thread::spawn; use rand::random; -type Server = Recv, Eps>>; +type Server = Recv, Eps)>>; type Client = ::Dual; fn server_handler(c: Chan<(), Server>) { @@ -34,12 +34,12 @@ fn server(rx: Receiver>) { fn client_handler(c: Chan<(), Client>) { let n = random(); match c.send(n).offer() { - Left(c) => { + B1(c) => { let (c, n2) = c.recv(); c.close(); println!("{} + 42 = {}", n, n2); }, - Right(c) => { + B2(c) => { c.close(); println!("{} + 42 is an overflow :(", n); } diff --git a/examples/planeclip.rs b/examples/planeclip.rs index f48168f..eec81af 100644 --- a/examples/planeclip.rs +++ b/examples/planeclip.rs @@ -53,8 +53,8 @@ fn intersect(p1: Point, p2: Point, plane: Plane) -> Option { } } -type SendList = Rec>>>; -type RecvList = Rec>>>; +type SendList = Rec>)>>; +type RecvList = Rec>)>>; fn sendlist (c: Chan<(), SendList>, xs: Vec) @@ -74,11 +74,11 @@ fn recvlist let mut c = c.enter(); loop { c = match c.offer() { - Left(c) => { + B1(c) => { c.close(); break; } - Right(c) => { + B2(c) => { let (c, x) = c.recv(); v.push(x); c.zero() @@ -98,12 +98,12 @@ fn clipper(plane: Plane, let (pt0, mut pt); match ic.offer() { - Left(c) => { + B1(c) => { c.close(); oc.sel1().close(); return } - Right(c) => { + B2(c) => { let (c, ptz) = c.recv(); ic = c.zero(); pt0 = ptz; @@ -116,7 +116,7 @@ fn clipper(plane: Plane, oc = oc.sel2().send(pt).zero(); } ic = match ic.offer() { - Left(c) => { + B1(c) => { if let Some(pt) = intersect(pt, pt0, plane) { oc = oc.sel2().send(pt).zero(); } @@ -124,7 +124,7 @@ fn clipper(plane: Plane, oc.sel1().close(); break; } - Right(ic) => { + B2(ic) => { let (ic, pt2) = ic.recv(); if let Some(pt) = intersect(pt, pt2, plane) { oc = oc.sel2().send(pt).zero(); diff --git a/src/branch_impls/Cargo.toml b/src/branch_impls/Cargo.toml new file mode 100644 index 0000000..8ec3ac4 --- /dev/null +++ b/src/branch_impls/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "branch_impls" +version = "0.1.0" +authors = [ "Philip Munksgaard " + ] + + +[lib] +plugin = true \ No newline at end of file diff --git a/src/branch_impls/src/lib.rs b/src/branch_impls/src/lib.rs new file mode 100644 index 0000000..291f2a7 --- /dev/null +++ b/src/branch_impls/src/lib.rs @@ -0,0 +1,139 @@ +#![crate_type="dylib"] +#![feature(plugin_registrar, rustc_private)] + +extern crate syntax; +extern crate rustc; +extern crate rustc_plugin; + +use syntax::codemap::Span; +use syntax::ptr::P; +use syntax::ast::{TokenTree, Item}; +use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager}; +use syntax::ext::quote::rt::ExtParseUtils; +use syntax::util::small_vector::SmallVector; +use syntax::ast::Lit_; +use rustc_plugin::Registry; + +fn commalist(n: u8, before: &str, after: &str) -> String { + let mut buf = "".to_string(); + for i in 1..n { + buf.push_str(&format!("{}B{}{}, ", before, i, after)); + } + buf.push_str(&format!("{}B{}{}", before, n, after)); + + return buf; +} + +fn branch_struct(n: u8) -> String { + let mut buf = format!("pub enum Branch{}<", n); + buf.push_str(&commalist(n, "", "")); + buf.push_str("> { "); + for i in 1..n+1 { + buf.push_str(&format!("B{}(B{}), ", i, i)); + } + + buf.push_str(" }"); + + return buf; +} + +fn hasdual_choose_impl(n: u8) -> String { + let mut buf = "unsafe impl <".to_string(); + buf.push_str(&commalist(n, "", ": HasDual")); + buf.push_str("> HasDual for Choose<("); + buf.push_str(&commalist(n, "", "")); + buf.push_str(")> { type Dual = Offer<("); + buf.push_str(&commalist(n, "", "::Dual")); + buf.push_str(")>; }"); + + return buf; +} + +fn hasdual_offer_impl(n: u8) -> String { + let mut buf = "unsafe impl <".to_string(); + buf.push_str(&commalist(n, "", ": HasDual")); + buf.push_str("> HasDual for Offer<("); + buf.push_str(&commalist(n, "", "")); + buf.push_str(")> { type Dual = Choose<("); + buf.push_str(&commalist(n, "", "::Dual")); + buf.push_str(")>; }"); + + return buf; +} + +fn choose_impl(n: u8) -> String { + let mut buf = "impl Chan> { "); + + for i in 1..n+1 { + buf.push_str("#[must_use] "); + buf.push_str(&format!("pub fn sel{}(self) -> Chan {{ ", i, i)); + buf.push_str(&format!("unsafe {{ write_chan(&self, {}); transmute(self) }} }} ", i)); + } + + buf.push_str("}"); + + return buf; +} + +fn offer_impl(n: u8) -> String { + let mut buf = "impl Chan> {{ #[must_use] pub fn offer(self) -> Branch{}<", n)); + buf.push_str(&commalist(n, "Chan")); + buf.push_str("> { unsafe { match read_chan(&self) { "); + + for i in 1..(n+1) { + buf.push_str(&format!("{}u8 => Branch{}::B{}(transmute(self)), ", i, n, i)); + } + + buf.push_str("_ => unreachable!() "); + buf.push_str("} } } }"); + + return buf; +} + +fn impl_branch(cx: &mut ExtCtxt, n: u8) -> SmallVector> { + let mut v = SmallVector::zero(); + + v.push(cx.parse_item(branch_struct(n))); + v.push(cx.parse_item(hasdual_choose_impl(n))); + v.push(cx.parse_item(hasdual_offer_impl(n))); + v.push(cx.parse_item(offer_impl(n))); + v.push(cx.parse_item(choose_impl(n))); + + return v; +} + +fn branch_impls(cx: &mut ExtCtxt, sp: Span, tts: &[TokenTree]) -> Box { + let mut parser = cx.new_parser_from_tts(tts); + let n = match parser.parse_lit() { + Ok(lit) => match lit.node { + Lit_::LitInt(n, _) => n, + _ => { + cx.span_err(lit.span, "Expected literal integer"); + return DummyResult::any(sp); + } + }, + Err(mut e) => { + e.emit(); + return DummyResult::any(sp); + } + } as u8; + + let mut v = SmallVector::zero(); + + for i in 2..n+1 { + v.push_all(impl_branch(cx, i)); + } + + return MacEager::items(v); +} +#[plugin_registrar] +pub fn plugin_registrar(reg: &mut Registry) { + reg.register_macro("branch_impls", branch_impls);} diff --git a/src/lib.rs b/src/lib.rs index 6db275e..2966e9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,9 @@ #![cfg_attr(feature = "chan_select", feature(mpsc_select))] +#![feature(plugin)] +#![plugin(branch_impls)] + use std::marker; use std::thread::spawn; use std::mem::transmute; @@ -73,7 +76,7 @@ use std::sync::mpsc::Select; #[cfg(feature = "chan_select")] use std::collections::HashMap; -pub use Branch::*; +pub use Branch2::*; /// A session typed channel. `P` is the protocol and `E` is the environment, /// containing potential recursion targets @@ -112,10 +115,9 @@ pub struct Recv ( PhantomData<(A, P)> ); pub struct Send ( PhantomData<(A, P)> ); /// Active choice between `P` and `Q` -pub struct Choose ( PhantomData<(P, Q)> ); +pub struct Choose ( PhantomData ); -/// Passive choice (offer) between `P` and `Q` -pub struct Offer ( PhantomData<(P, Q)> ); +pub struct Offer ( PhantomData ); /// Enter a recursive environment pub struct Rec

( PhantomData

); @@ -140,14 +142,6 @@ unsafe impl HasDual for Recv { type Dual = Send; } -unsafe impl HasDual for Choose { - type Dual = Offer; -} - -unsafe impl HasDual for Offer { - type Dual = Choose; -} - unsafe impl HasDual for Var { type Dual = Var; } @@ -160,11 +154,6 @@ unsafe impl HasDual for Rec

{ type Dual = Rec; } -pub enum Branch { - Left(L), - Right(R) -} - impl Drop for Chan { fn drop(&mut self) { panic!("Session channel prematurely dropped"); @@ -223,101 +212,6 @@ impl Chan> { } } -impl Chan> { - /// Perform an active choice, selecting protocol `P`. - #[must_use] - pub fn sel1(self) -> Chan { - unsafe { - write_chan(&self, true); - transmute(self) - } - } - - /// Perform an active choice, selecting protocol `Q`. - #[must_use] - pub fn sel2(self) -> Chan { - unsafe { - write_chan(&self, false); - transmute(self) - } - } -} - -/// Convenience function. This is identical to `.sel2()` -impl Chan> { - #[must_use] - pub fn skip(self) -> Chan { - self.sel2() - } -} - -/// Convenience function. This is identical to `.sel2().sel2()` -impl Chan>> { - #[must_use] - pub fn skip2(self) -> Chan { - self.sel2().sel2() - } -} - -/// Convenience function. This is identical to `.sel2().sel2().sel2()` -impl Chan>>> { - #[must_use] - pub fn skip3(self) -> Chan { - self.sel2().sel2().sel2() - } -} - -/// Convenience function. This is identical to `.sel2().sel2().sel2().sel2()` -impl Chan>>>> { - #[must_use] - pub fn skip4(self) -> Chan { - self.sel2().sel2().sel2().sel2() - } -} - -/// Convenience function. This is identical to `.sel2().sel2().sel2().sel2().sel2()` -impl Chan>>>>> { - #[must_use] - pub fn skip5(self) -> Chan { - self.sel2().sel2().sel2().sel2().sel2() - } -} - -/// Convenience function. -impl Chan>>>>>> { - #[must_use] - pub fn skip6(self) -> Chan { - self.sel2().sel2().sel2().sel2().sel2().sel2() - } -} - -/// Convenience function. -impl Chan>>>>>>> { - #[must_use] - pub fn skip7(self) -> Chan { - self.sel2().sel2().sel2().sel2().sel2().sel2().sel2() - } -} - -impl Chan> { - /// Passive choice. This allows the other end of the channel to select one - /// of two options for continuing the protocol: either `P` or `Q`. - #[must_use] - pub fn offer(self) -> Branch, Chan> { - unsafe { - let b = read_chan(&self); - if b { - Left(transmute(self)) - } else { - Right(transmute(self)) - } - } - } -} - impl Chan> { /// Enter a recursive environment, putting the current environment on the /// top of the environment stack. @@ -343,6 +237,8 @@ impl Chan<(P, E), Var>> { } } +branch_impls!(30); + /// Homogeneous select. We have a vector of channels, all obeying the same /// protocol (and in the exact same point of the protocol), wait for one of them /// to receive. Removes the receiving channel from the vector and returns both @@ -423,7 +319,7 @@ impl<'c, T> ChanSelect<'c, T> { } pub fn add_offer_ret(&mut self, - chan: &'c Chan>, + chan: &'c Chan>, ret: T) { self.chans.push((unsafe { transmute(chan) }, ret)); @@ -477,7 +373,7 @@ impl<'c> ChanSelect<'c, usize> { } pub fn add_offer(&mut self, - c: &'c Chan>) + c: &'c Chan>) { let index = self.chans.len(); self.add_offer_ret(c, index); @@ -509,68 +405,6 @@ pub fn connect(srv: F1, cli: F2) t.join().unwrap(); } -/// This macro is convenient for server-like protocols of the form: -/// -/// `Offer>>` -/// -/// # Examples -/// -/// Assume we have a protocol `Offer, Offer,Eps>>>` -/// we can use the `offer!` macro as follows: -/// -/// ```rust -/// #[macro_use] extern crate session_types; -/// use session_types::*; -/// use std::thread::spawn; -/// -/// fn srv(c: Chan<(), Offer, Offer, Eps>>>) { -/// offer! { c, -/// Number => { -/// let (c, n) = c.recv(); -/// assert_eq!(42, n); -/// c.close(); -/// }, -/// String => { -/// c.recv().0.close(); -/// }, -/// Quit => { -/// c.close(); -/// } -/// } -/// } -/// -/// fn cli(c: Chan<(), Choose, Choose, Eps>>>) { -/// c.sel1().send(42).close(); -/// } -/// -/// fn main() { -/// let (s, c) = session_channel(); -/// spawn(move|| cli(c)); -/// srv(s); -/// } -/// ``` -/// -/// The identifiers on the left-hand side of the arrows have no semantic -/// meaning, they only provide a meaningful name for the reader. -#[macro_export] -macro_rules! offer { - ( - $id:ident, $branch:ident => $code:expr, $($t:tt)+ - ) => ( - match $id.offer() { - Left($id) => $code, - Right($id) => offer!{ $id, $($t)+ } - } - ); - ( - $id:ident, $branch:ident => $code:expr - ) => ( - $code - ) -} - -/// This macro plays the same role as the `select!` macro does for `Receiver`s. -/// /// It also supports a second form with `Offer`s (see the example below). /// /// # Examples @@ -610,74 +444,6 @@ macro_rules! offer { /// } /// } /// ``` -/// -/// ```rust -/// #![feature(rand)] -/// #[macro_use] -/// extern crate session_types; -/// extern crate rand; -/// -/// use std::thread::spawn; -/// use session_types::*; -/// -/// type Igo = Choose, Send>; -/// type Ugo = Offer, Recv>; -/// -/// fn srv(chan_one: Chan<(), Ugo>, chan_two: Chan<(), Ugo>) { -/// let _ign; -/// chan_select! { -/// _ign = chan_one.offer() => { -/// String => { -/// let (c, s) = chan_one.recv(); -/// assert_eq!("Hello, World!".to_string(), s); -/// c.close(); -/// match chan_two.offer() { -/// Left(c) => c.recv().0.close(), -/// Right(c) => c.recv().0.close(), -/// } -/// }, -/// Number => { -/// chan_one.recv().0.close(); -/// match chan_two.offer() { -/// Left(c) => c.recv().0.close(), -/// Right(c) => c.recv().0.close(), -/// } -/// } -/// }, -/// _ign = chan_two.offer() => { -/// String => { -/// chan_two.recv().0.close(); -/// match chan_one.offer() { -/// Left(c) => c.recv().0.close(), -/// Right(c) => c.recv().0.close(), -/// } -/// }, -/// Number => { -/// chan_two.recv().0.close(); -/// match chan_one.offer() { -/// Left(c) => c.recv().0.close(), -/// Right(c) => c.recv().0.close(), -/// } -/// } -/// } -/// } -/// } -/// -/// fn cli(c: Chan<(), Igo>) { -/// c.sel1().send("Hello, World!".to_string()).close(); -/// } -/// -/// fn main() { -/// let (ca1, ca2) = session_channel(); -/// let (cb1, cb2) = session_channel(); -/// -/// cb2.sel2().send(42).close(); -/// -/// spawn(move|| cli(ca2)); -/// -/// srv(ca1, cb1); -/// } -/// ``` #[cfg(features = "chan_select")] #[macro_export] macro_rules! chan_select { @@ -694,17 +460,4 @@ macro_rules! chan_select { else )+ { unreachable!() } }); - - ( - $($res:ident = $rx:ident.offer() => { $($t:tt)+ }),+ - ) => ({ - let index = { - let mut sel = $crate::ChanSelect::new(); - $( sel.add_offer(&$rx); )+ - sel.wait() - }; - let mut i = 0; - $( if index == { i += 1; i - 1 } { $res = offer!{ $rx, $($t)+ } } else )+ - { unreachable!() } - }) }