use proc_macro::TokenStream;
use quote::quote;
use syn::{
Data, DeriveInput, Error, Expr, ExprLit, Fields, GenericArgument, ItemStruct, Lit,
PathArguments, Type, TypeArray, TypePath, parse_macro_input, spanned::Spanned,
};
fn extract_fixed_byte_array_size(ty: &Type) -> Option<i32> {
if let Type::Array(TypeArray { elem, len, .. }) = ty {
if let Type::Path(TypePath { path, .. }) = elem.as_ref()
&& let Some(last_segment) = path.segments.last()
&& last_segment.ident == "u8"
{
if let Expr::Lit(ExprLit {
lit: Lit::Int(int_lit),
..
}) = len
&& let Ok(length) = int_lit.base10_parse::<i32>()
{
return Some(length);
}
}
}
None
}
fn extract_option_inner_type(ty: &Type) -> Option<&Type> {
if let Type::Path(TypePath { qself: None, path }) = ty {
if let Some(last_segment) = path.segments.last()
&& last_segment.ident == "Option"
{
if let PathArguments::AngleBracketed(ref args) = last_segment.arguments
&& args.args.len() == 1
&& let Some(GenericArgument::Type(inner_ty)) = args.args.first()
{
return Some(inner_ty);
}
}
}
None
}
fn is_vec_u8(ty: &Type) -> bool {
if let Type::Path(TypePath { path, .. }) = ty
&& let Some(last_segment) = path.segments.last()
&& last_segment.ident == "Vec"
&& let PathArguments::AngleBracketed(ref args) = last_segment.arguments
&& args.args.len() == 1
&& let Some(GenericArgument::Type(Type::Path(TypePath {
path: inner_path, ..
}))) = args.args.first()
&& let Some(seg) = inner_path.segments.last()
{
return seg.ident == "u8";
}
false
}
fn has_as_binary_attr(field: &syn::Field) -> bool {
field.attrs.iter().any(|attr| {
if !attr.path().is_ident("prestige") {
return false;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("as_binary") {
found = true;
}
Ok(())
});
found
})
}
fn has_serde_bytes_attr(field: &syn::Field) -> bool {
field.attrs.iter().any(|attr| {
if !attr.path().is_ident("serde") {
return false;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("with")
&& let Ok(value) = meta.value()
&& let Ok(lit) = value.parse::<syn::LitStr>()
{
let val = lit.value();
if val == "serde_bytes" || val.ends_with("::serde_bytes") {
found = true;
}
}
Ok(())
});
found
})
}
fn extract_named_fields(
input: &DeriveInput,
) -> Result<&syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, Error> {
match input.data {
Data::Struct(ref data) => match data.fields {
Fields::Named(ref fields) => Ok(&fields.named),
_ => Err(Error::new(
input.span(),
"Can only be derived for structs with named fields",
)),
},
_ => Err(Error::new(input.span(), "Can only be derived for structs")),
}
}
fn has_identifier_attr(field: &syn::Field) -> bool {
field.attrs.iter().any(|attr| {
if !attr.path().is_ident("prestige") {
return false;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("identifier") {
found = true;
}
Ok(())
});
found
})
}
fn collect_identifier_field_names(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Vec<String> {
fields
.iter()
.filter(|f| has_identifier_attr(f))
.map(|f| f.ident.as_ref().unwrap().to_string())
.collect()
}
fn validate_identifier_fields(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Result<(), Error> {
for field in fields {
if !has_identifier_attr(field) {
continue;
}
if extract_option_inner_type(&field.ty).is_some() {
return Err(Error::new(
field.span(),
"identifier fields must be required (not Option<T>)",
));
}
}
Ok(())
}
#[derive(Debug)]
struct SortKeyAttr {
descending: bool,
explicit_order: Option<u32>,
}
#[derive(Debug)]
enum PartitionTransform {
Identity,
Year,
Month,
Day,
Hour,
Bucket(u32),
Truncate(u32),
}
fn extract_sort_key_attr(field: &syn::Field) -> Option<SortKeyAttr> {
for attr in &field.attrs {
if !attr.path().is_ident("prestige") {
continue;
}
let mut found_sort_key = false;
let mut descending = false;
let mut explicit_order: Option<u32> = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("sort_key") {
found_sort_key = true;
if meta.input.peek(syn::token::Paren) {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("desc") {
descending = true;
} else if inner.path.is_ident("asc") {
descending = false;
} else if inner.path.is_ident("order") {
let value = inner.value()?;
let lit: syn::LitInt = value.parse()?;
explicit_order = Some(lit.base10_parse()?);
}
Ok(())
})?;
}
}
Ok(())
});
if found_sort_key {
return Some(SortKeyAttr {
descending,
explicit_order,
});
}
}
None
}
fn extract_partition_attr(field: &syn::Field) -> Result<Option<PartitionTransform>, Error> {
for attr in &field.attrs {
if !attr.path().is_ident("prestige") {
continue;
}
let mut found_partition = false;
let mut transform = PartitionTransform::Identity;
let mut parse_error: Option<Error> = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("partition") {
found_partition = true;
if meta.input.peek(syn::token::Paren) {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("year") {
transform = PartitionTransform::Year;
} else if inner.path.is_ident("month") {
transform = PartitionTransform::Month;
} else if inner.path.is_ident("day") {
transform = PartitionTransform::Day;
} else if inner.path.is_ident("hour") {
transform = PartitionTransform::Hour;
} else if inner.path.is_ident("bucket") {
let content;
syn::parenthesized!(content in inner.input);
let lit: syn::LitInt = content.parse()?;
let n: u32 = lit.base10_parse()?;
transform = PartitionTransform::Bucket(n);
} else if inner.path.is_ident("truncate") {
let content;
syn::parenthesized!(content in inner.input);
let lit: syn::LitInt = content.parse()?;
let w: u32 = lit.base10_parse()?;
transform = PartitionTransform::Truncate(w);
} else {
parse_error = Some(Error::new(
inner.path.span(),
"unknown partition transform; expected: year, month, day, hour, bucket(N), truncate(W)",
));
}
Ok(())
})?;
}
}
Ok(())
});
if let Some(err) = parse_error {
return Err(err);
}
if found_partition {
return Ok(Some(transform));
}
}
Ok(None)
}
fn collect_sort_field_defs(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Vec<(String, SortKeyAttr, usize)> {
fields
.iter()
.enumerate()
.filter_map(|(pos, f)| {
extract_sort_key_attr(f).map(|attr| (f.ident.as_ref().unwrap().to_string(), attr, pos))
})
.collect()
}
fn collect_partition_field_defs(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Result<Vec<(String, PartitionTransform)>, Error> {
let mut result = Vec::new();
for field in fields {
if let Some(transform) = extract_partition_attr(field)? {
result.push((field.ident.as_ref().unwrap().to_string(), transform));
}
}
Ok(result)
}
#[derive(Debug, Default)]
struct StructPrestigeAttrs {
table_name: Option<String>,
namespace: Option<Vec<String>>,
}
fn generate_iceberg_schema_impl(
name: &syn::Ident,
struct_attrs: &StructPrestigeAttrs,
identifier_names: &[String],
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Result<proc_macro2::TokenStream, Error> {
let sort_defs = collect_sort_field_defs(fields);
let partition_defs = collect_partition_field_defs(fields)?;
let sort_order_impl = if sort_defs.is_empty() {
quote! { None }
} else {
let sort_entries: Vec<proc_macro2::TokenStream> = sort_defs
.iter()
.map(|(field_name, attr, pos)| {
let order = attr.explicit_order.unwrap_or(*pos as u32);
let (dir, null) = if attr.descending {
(
quote! { ::prestige::iceberg::SortDirection::Descending },
quote! { ::prestige::iceberg::NullOrder::Last },
)
} else {
(
quote! { ::prestige::iceberg::SortDirection::Ascending },
quote! { ::prestige::iceberg::NullOrder::First },
)
};
quote! {
::prestige::iceberg::SortFieldDef {
name: #field_name,
direction: #dir,
null_order: #null,
order: #order,
}
}
})
.collect();
quote! {
{
let schema = Self::iceberg_schema();
let defs = &[#(#sort_entries),*];
Some(::prestige::iceberg::build_sort_order(&schema, defs).expect("sort order fields must exist in schema"))
}
}
};
let partition_spec_impl = if partition_defs.is_empty() {
quote! { None }
} else {
let partition_entries: Vec<proc_macro2::TokenStream> = partition_defs
.iter()
.map(|(field_name, transform)| {
let transform_expr = match transform {
PartitionTransform::Identity => {
quote! { ::prestige::iceberg::Transform::Identity }
}
PartitionTransform::Year => quote! { ::prestige::iceberg::Transform::Year },
PartitionTransform::Month => quote! { ::prestige::iceberg::Transform::Month },
PartitionTransform::Day => quote! { ::prestige::iceberg::Transform::Day },
PartitionTransform::Hour => quote! { ::prestige::iceberg::Transform::Hour },
PartitionTransform::Bucket(n) => {
quote! { ::prestige::iceberg::Transform::Bucket(#n) }
}
PartitionTransform::Truncate(w) => {
quote! { ::prestige::iceberg::Transform::Truncate(#w) }
}
};
quote! {
::prestige::iceberg::PartitionFieldDef {
name: #field_name,
transform: #transform_expr,
}
}
})
.collect();
quote! {
{
let schema = Self::iceberg_schema();
let defs = &[#(#partition_entries),*];
Some(::prestige::iceberg::build_partition_spec(&schema, defs).expect("partition fields must exist in schema"))
}
}
};
let table_name_impl = match &struct_attrs.table_name {
Some(name) => quote! { Some(#name) },
None => quote! { None },
};
let namespace_impl = match &struct_attrs.namespace {
Some(parts) => {
quote! { Some(&[#(#parts),*]) }
}
None => quote! { None },
};
let identifier_impl = quote! { &[#(#identifier_names),*] };
Ok(quote! {
#[cfg(feature = "iceberg")]
impl ::prestige::iceberg::IcebergSchema for #name {
fn iceberg_schema() -> ::prestige::iceberg::Schema {
::prestige::iceberg::arrow_to_iceberg_schema_with_identifiers(
&<Self as ::prestige::ArrowSchema>::arrow_schema(),
Self::identifier_field_names(),
).expect("arrow schema must convert to iceberg schema")
}
fn table_partition_spec() -> Option<::prestige::iceberg::UnboundPartitionSpec> {
#partition_spec_impl
}
fn table_sort_order() -> Option<::prestige::iceberg::SortOrder> {
#sort_order_impl
}
fn default_table_name() -> Option<&'static str> {
#table_name_impl
}
fn default_namespace() -> Option<&'static [&'static str]> {
#namespace_impl
}
fn identifier_field_names() -> &'static [&'static str] {
#identifier_impl
}
}
})
}
fn generate_sort_and_partition_impl(
name: &syn::Ident,
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Result<proc_macro2::TokenStream, Error> {
let sort_defs = collect_sort_field_defs(fields);
let partition_defs = collect_partition_field_defs(fields)?;
let sort_entries: Vec<proc_macro2::TokenStream> = sort_defs
.iter()
.map(|(field_name, attr, pos)| {
let order = attr.explicit_order.unwrap_or(*pos as u32);
let (dir, null) = if attr.descending {
(
quote! { ::prestige::iceberg::SortDirection::Descending },
quote! { ::prestige::iceberg::NullOrder::Last },
)
} else {
(
quote! { ::prestige::iceberg::SortDirection::Ascending },
quote! { ::prestige::iceberg::NullOrder::First },
)
};
quote! {
::prestige::iceberg::SortFieldDef {
name: #field_name,
direction: #dir,
null_order: #null,
order: #order,
}
}
})
.collect();
let partition_entries: Vec<proc_macro2::TokenStream> = partition_defs
.iter()
.map(|(field_name, transform)| {
let transform_expr = match transform {
PartitionTransform::Identity => quote! { ::prestige::iceberg::Transform::Identity },
PartitionTransform::Year => quote! { ::prestige::iceberg::Transform::Year },
PartitionTransform::Month => quote! { ::prestige::iceberg::Transform::Month },
PartitionTransform::Day => quote! { ::prestige::iceberg::Transform::Day },
PartitionTransform::Hour => quote! { ::prestige::iceberg::Transform::Hour },
PartitionTransform::Bucket(n) => {
quote! { ::prestige::iceberg::Transform::Bucket(#n) }
}
PartitionTransform::Truncate(w) => {
quote! { ::prestige::iceberg::Transform::Truncate(#w) }
}
};
quote! {
::prestige::iceberg::PartitionFieldDef {
name: #field_name,
transform: #transform_expr,
}
}
})
.collect();
Ok(quote! {
#[cfg(feature = "iceberg")]
impl #name {
pub fn sort_field_definitions() -> &'static [::prestige::iceberg::SortFieldDef] {
&[#(#sort_entries),*]
}
pub fn partition_field_definitions() -> &'static [::prestige::iceberg::PartitionFieldDef] {
&[#(#partition_entries),*]
}
}
})
}
fn generate_arrow_field_schemas(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Vec<proc_macro2::TokenStream> {
fields
.iter()
.map(|field| {
let field_name = field.ident.as_ref().unwrap().to_string();
let field_type = &field.ty;
let is_binary = has_as_binary_attr(field);
let (inner_type, nullable) = match extract_option_inner_type(field_type) {
Some(inner) => (inner, true),
None => (field_type, false),
};
let data_type_expr = if is_binary {
if let Some(size) = extract_fixed_byte_array_size(inner_type) {
quote! { arrow::datatypes::DataType::FixedSizeBinary(#size) }
} else if is_vec_u8(inner_type) {
quote! { arrow::datatypes::DataType::Binary }
} else {
quote! { <#inner_type as ::prestige::ArrowSerialize>::arrow_data_type() }
}
} else {
quote! { <#inner_type as ::prestige::ArrowSerialize>::arrow_data_type() }
};
quote! {
arrow::datatypes::Field::new(#field_name, #data_type_expr, #nullable)
}
})
.collect()
}
fn generate_arrow_field_bounds(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Vec<proc_macro2::TokenStream> {
fields
.iter()
.map(|field| {
let field_type = &field.ty;
if let Some(inner_type) = extract_option_inner_type(field_type) {
quote! { #inner_type: ::prestige::ArrowSerialize }
} else {
quote! { #field_type: ::prestige::ArrowSerialize }
}
})
.collect()
}
fn generate_arrow_group_impl(
name: &syn::Ident,
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> proc_macro2::TokenStream {
let arrow_field_schemas = generate_arrow_field_schemas(fields);
let arrow_field_bounds = generate_arrow_field_bounds(fields);
quote! {
impl #name
where
#(#arrow_field_bounds),*
{
pub fn arrow_schema() -> arrow::datatypes::Schema {
arrow::datatypes::Schema::new(vec![
#(#arrow_field_schemas),*
])
}
}
}
}
fn generate_arrow_reader_impl(name: &syn::Ident) -> proc_macro2::TokenStream {
quote! {
impl #name
where
Self: for<'de> serde::Deserialize<'de>,
{
pub fn from_arrow_records(
arrays: &[std::sync::Arc<dyn arrow::array::Array>],
schema: &arrow::datatypes::Schema,
) -> Result<Vec<Self>, serde_arrow::Error> {
serde_arrow::from_arrow(schema.fields(), arrays)
}
pub fn from_arrow_reader<R: std::io::Read + std::io::Seek>(
reader: R,
) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
use arrow::ipc::reader::FileReader;
let arrow_reader = FileReader::try_new(reader, None)?;
let schema = arrow_reader.schema();
let mut records = Vec::new();
for batch_result in arrow_reader {
let batch = batch_result?;
let arrays: Vec<std::sync::Arc<dyn arrow::array::Array>> =
(0..batch.num_columns())
.map(|i| batch.column(i).clone())
.collect();
let batch_records = Self::from_arrow_records(&arrays, &schema)?;
records.extend(batch_records);
}
Ok(records)
}
}
}
}
fn generate_arrow_writer_impl(name: &syn::Ident) -> proc_macro2::TokenStream {
quote! {
impl #name
where
Self: serde::Serialize,
{
pub fn to_arrow_arrays(
records: &[Self],
) -> Result<(Vec<std::sync::Arc<dyn arrow::array::Array>>, arrow::datatypes::Schema), serde_arrow::Error> {
if records.is_empty() {
return Ok((Vec::new(), Self::arrow_schema()));
}
let arrow_schema = Self::arrow_schema();
let arrays = serde_arrow::to_arrow(arrow_schema.fields(), records)?;
Ok((arrays, arrow_schema))
}
pub fn write_arrow_file<W: std::io::Write + std::io::Seek>(
records: &[Self],
writer: W,
) -> Result<(), Box<dyn std::error::Error>> {
use arrow::ipc::writer::FileWriter;
let (arrays, schema) = Self::to_arrow_arrays(records)?;
let batch = arrow::record_batch::RecordBatch::try_new(
std::sync::Arc::new(schema.clone()),
arrays,
)?;
let mut arrow_writer = FileWriter::try_new(writer, &schema)?;
arrow_writer.write(&batch)?;
arrow_writer.finish()?;
Ok(())
}
pub fn write_arrow_stream<W: std::io::Write>(
records: &[Self],
writer: W,
) -> Result<(), Box<dyn std::error::Error>> {
use arrow::ipc::writer::StreamWriter;
let (arrays, schema) = Self::to_arrow_arrays(records)?;
let batch = arrow::record_batch::RecordBatch::try_new(
std::sync::Arc::new(schema.clone()),
arrays,
)?;
let mut arrow_writer = StreamWriter::try_new(writer, &schema)?;
arrow_writer.write(&batch)?;
arrow_writer.finish()?;
Ok(())
}
}
}
}
#[proc_macro_derive(ArrowGroup)]
pub fn derive_arrow_group(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = match extract_named_fields(&input) {
Ok(fields) => fields,
Err(err) => return err.to_compile_error().into(),
};
let expanded = generate_arrow_group_impl(name, fields);
TokenStream::from(expanded)
}
#[proc_macro_derive(ArrowReader)]
pub fn derive_arrow_reader(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = generate_arrow_reader_impl(name);
TokenStream::from(expanded)
}
#[proc_macro_derive(ArrowWriter)]
pub fn derive_arrow_writer(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = generate_arrow_writer_impl(name);
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn prestige_schema(args: TokenStream, input: TokenStream) -> TokenStream {
let mut item = parse_macro_input!(input as ItemStruct);
let struct_attrs = match syn::parse::Parser::parse(
|input: syn::parse::ParseStream| {
let mut result = StructPrestigeAttrs::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let _: syn::Token![=] = input.parse()?;
let lit: syn::LitStr = input.parse()?;
if ident == "table" {
result.table_name = Some(lit.value());
} else if ident == "namespace" {
result.namespace = Some(lit.value().split('.').map(String::from).collect());
} else {
return Err(syn::Error::new(
ident.span(),
format!("unknown prestige_schema argument: {ident}"),
));
}
let _ = input.parse::<syn::Token![,]>();
}
Ok(result)
},
args,
) {
Ok(attrs) => attrs,
Err(err) => return err.to_compile_error().into(),
};
let fields = match &mut item.fields {
Fields::Named(named) => named,
_ => {
return syn::Error::new(item.ident.span(), "prestige_schema requires named fields")
.to_compile_error()
.into();
}
};
{
let has_serialize = item.attrs.iter().any(|attr| {
if !attr.path().is_ident("derive") {
return false;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("Serialize")
|| meta.path.segments.len() == 2
&& meta.path.segments[0].ident == "serde"
&& meta.path.segments[1].ident == "Serialize"
{
found = true;
}
Ok(())
});
found
});
let has_deserialize = item.attrs.iter().any(|attr| {
if !attr.path().is_ident("derive") {
return false;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("Deserialize")
|| meta.path.segments.len() == 2
&& meta.path.segments[0].ident == "serde"
&& meta.path.segments[1].ident == "Deserialize"
{
found = true;
}
Ok(())
});
found
});
if !has_serialize || !has_deserialize {
let derive_attr: syn::Attribute = if !has_serialize && !has_deserialize {
syn::parse_quote! { #[derive(serde::Serialize, serde::Deserialize)] }
} else if !has_serialize {
syn::parse_quote! { #[derive(serde::Serialize)] }
} else {
syn::parse_quote! { #[derive(serde::Deserialize)] }
};
item.attrs.push(derive_attr);
}
}
for field in fields.named.iter_mut() {
if has_serde_bytes_attr(field) {
continue; }
let is_binary = has_as_binary_attr(field);
let ty = &field.ty;
let (inner_ty, is_option) = match extract_option_inner_type(ty) {
Some(inner) => (inner.clone(), true),
None => (ty.clone(), false),
};
let is_fixed_u8 = extract_fixed_byte_array_size(&inner_ty).is_some();
let is_vec_u8_field = is_vec_u8(&inner_ty);
if is_binary && (is_fixed_u8 || is_vec_u8_field) {
field.attrs.push(syn::parse_quote! {
#[serde(with = "::prestige::serde_bytes")]
});
} else if !is_binary && is_fixed_u8 {
if is_option {
field.attrs.push(syn::parse_quote! {
#[serde(with = "::prestige::serde_u8_array::option")]
});
} else {
field.attrs.push(syn::parse_quote! {
#[serde(with = "::prestige::serde_u8_array")]
});
}
}
}
if let Err(err) = validate_identifier_fields(&fields.named) {
return err.to_compile_error().into();
}
let identifier_names = collect_identifier_field_names(&fields.named);
let name = &item.ident;
let sort_and_partition = match generate_sort_and_partition_impl(name, &fields.named) {
Ok(tokens) => tokens,
Err(err) => return err.to_compile_error().into(),
};
let iceberg_schema_impl =
match generate_iceberg_schema_impl(name, &struct_attrs, &identifier_names, &fields.named) {
Ok(tokens) => tokens,
Err(err) => return err.to_compile_error().into(),
};
let arrow_group = generate_arrow_group_impl(name, &fields.named);
let arrow_reader = generate_arrow_reader_impl(name);
let arrow_writer = generate_arrow_writer_impl(name);
for field in fields.named.iter_mut() {
field.attrs.retain(|attr| !attr.path().is_ident("prestige"));
}
let expanded = quote! {
#item
#arrow_group
impl ::prestige::ArrowSchema for #name {
fn arrow_schema() -> arrow::datatypes::SchemaRef {
std::sync::Arc::new(Self::arrow_schema())
}
}
impl #name {
pub fn identifier_field_names() -> &'static [&'static str] {
&[#(#identifier_names),*]
}
}
#sort_and_partition
#iceberg_schema_impl
#arrow_reader
#arrow_writer
};
TokenStream::from(expanded)
}