little refactoring

This commit is contained in:
vms 2021-04-02 18:31:19 +03:00
parent ea11a617cd
commit 12685c59fc
18 changed files with 164 additions and 138 deletions

View File

@ -33,7 +33,7 @@ use proc_macro_error::proc_macro_error;
use syn::spanned::Spanned;
/// This macro allows user to write tests for services in the following form:
///```ignore
///```rust
/// #[fce_test(config = "/path/to/Config.toml", modules_dir = "path/to/service/modules")]
/// fn test() {
/// let service_result = greeting.greeting("John".to_string());

View File

@ -19,13 +19,19 @@ use crate::parsed_type::ParsedType;
use serde::Serialize;
use serde::Deserialize;
#[derive(Clone, Serialize, Deserialize)]
pub struct AstFuncArgument {
pub name: String,
pub ty: ParsedType,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AstFunctionSignature {
// Option is needed only for skipping serialization/deserialization of syn::ItemFn
#[serde(skip)]
pub visibility: Option<syn::Visibility>,
pub name: String,
pub arguments: Vec<(String, ParsedType)>,
pub arguments: Vec<AstFuncArgument>,
// fce supports only one return value now,
// waiting for adding multi-value support in Wasmer.
pub output_type: Option<ParsedType>,

View File

@ -16,7 +16,10 @@
use super::ParseMacroInput;
use crate::fce_ast_types;
use crate::fce_ast_types::{FCEAst, AstFunctionItem};
use crate::fce_ast_types::FCEAst;
use crate::fce_ast_types::AstFunctionItem;
use crate::fce_ast_types::AstFuncArgument;
use crate::syn_error;
use syn::Result;
@ -45,7 +48,7 @@ pub(super) fn try_to_ast_signature(
let arguments = inputs
.iter()
.map(|arg| -> Result<(String, ParsedType)> {
.map(|arg| -> Result<_> {
let pat = match arg {
syn::FnArg::Typed(arg) => arg,
_ => {
@ -55,18 +58,21 @@ pub(super) fn try_to_ast_signature(
))
}
};
Ok((
pat.pat
.to_token_stream()
.to_string()
.split(' ')
.last()
.unwrap_or_default()
.to_string(),
ParsedType::from_type(pat.ty.as_ref())?,
))
let name = pat
.pat
.to_token_stream()
.to_string()
.split(' ')
.last()
.unwrap_or_default()
.to_string();
let ty = ParsedType::from_type(pat.ty.as_ref())?;
let ast_arg = AstFuncArgument { name, ty };
Ok(ast_arg)
})
.collect::<Result<Vec<(_, _)>>>()?;
.collect::<Result<Vec<_>>>()?;
let output_type = ParsedType::from_return_type(&output)?;
@ -81,8 +87,8 @@ pub(super) fn try_to_ast_signature(
}
/// Check whether the #[fce] macro could be applied to a function.
#[rustfmt::skip]
fn check_function(signature: &syn::Signature) -> Result<()> {
use syn::Error;
use syn::spanned::Spanned;
let syn::Signature {
@ -95,34 +101,19 @@ fn check_function(signature: &syn::Signature) -> Result<()> {
} = signature;
if let Some(constness) = constness {
return Err(Error::new(
constness.span,
"FCE export function shouldn't be constant",
));
return syn_error!(constness.span, "FCE export function shouldn't be constant");
}
if let Some(unsafety) = unsafety {
return Err(Error::new(
unsafety.span,
"FCE export function shouldn't be unsafe",
));
return syn_error!(unsafety.span, "FCE export function shouldn't be unsafe");
}
if let Some(abi) = abi {
return Err(Error::new(
abi.extern_token.span,
"FCE export function shouldn't have any custom linkage",
));
return syn_error!(abi.extern_token.span, "FCE export function shouldn't have any custom linkage");
}
if generics.where_clause.is_some() {
return Err(Error::new(
signature.span(),
"FCE export function shouldn't use template parameters",
));
return syn_error!(signature.span(), "FCE export function shouldn't use template parameters");
}
if variadic.is_some() {
return Err(Error::new(
variadic.span(),
"FCE export function shouldn't use variadic interface",
));
return syn_error!(variadic.span(), "FCE export function shouldn't use variadic interface");
}
// TODO: check for a lifetime

View File

@ -17,8 +17,8 @@
use super::ParseMacroInput;
use crate::fce_ast_types;
use crate::fce_ast_types::FCEAst;
use crate::syn_error;
use syn::Error;
use syn::Result;
use syn::spanned::Spanned;
@ -30,7 +30,7 @@ impl ParseMacroInput for syn::ItemForeignMod {
fn parse_macro_input(self) -> Result<FCEAst> {
match &self.abi.name {
Some(name) if name.value() != "C" => {
return Err(Error::new(self.span(), "only 'C' abi is allowed"))
return syn_error!(self.span(), "only 'C' abi is allowed")
}
_ => {}
};
@ -69,10 +69,10 @@ impl ParseMacroInput for syn::ItemForeignMod {
.collect();
match wasm_import_module {
Some(namespace) if namespace.is_empty() => Err(Error::new(
Some(namespace) if namespace.is_empty() => syn_error!(
self_span,
"import module name should be defined by 'wasm_import_module' directive",
)),
"import module name should be defined by 'wasm_import_module' directive"
),
Some(namespace) => {
let extern_mod_item = fce_ast_types::AstExternModItem {
namespace,
@ -81,10 +81,10 @@ impl ParseMacroInput for syn::ItemForeignMod {
};
Ok(FCEAst::ExternMod(extern_mod_item))
}
None => Err(Error::new(
None => syn_error!(
self_span,
"import module name should be defined by 'wasm_import_module' directive",
)),
"import module name should be defined by 'wasm_import_module' directive"
),
}
}
}
@ -93,10 +93,10 @@ fn parse_raw_foreign_item(raw_item: syn::ForeignItem) -> Result<fce_ast_types::A
let function_item = match raw_item {
syn::ForeignItem::Fn(function_item) => function_item,
_ => {
return Err(Error::new(
return syn_error!(
raw_item.span(),
"#[fce] could be applied only to a function, struct ot extern block",
))
"#[fce] could be applied only to a function, struct ot extern block"
)
}
};
@ -117,10 +117,10 @@ fn parse_raw_foreign_item(raw_item: syn::ForeignItem) -> Result<fce_ast_types::A
None => None,
};
let function = super::item_fn::try_to_ast_signature(function_item.sig, function_item.vis)?;
let signature = super::item_fn::try_to_ast_signature(function_item.sig, function_item.vis)?;
let ast_extern_fn_item = fce_ast_types::AstExternFnItem {
link_name,
signature: function,
signature,
};
Ok(ast_extern_fn_item)

View File

@ -15,13 +15,14 @@
*/
use super::ParseMacroInput;
use crate::{fce_ast_types, AstRecordField};
use crate::fce_ast_types;
use crate::AstRecordField;
use crate::fce_ast_types::FCEAst;
use crate::syn_error;
use crate::parsed_type::ParsedType;
use syn::Error;
use syn::Result;
use syn::spanned::Spanned;
use crate::parsed_type::ParsedType;
impl ParseMacroInput for syn::ItemStruct {
fn parse_macro_input(self) -> Result<FCEAst> {
@ -29,29 +30,10 @@ impl ParseMacroInput for syn::ItemStruct {
let fields = match &self.fields {
syn::Fields::Named(named_fields) => &named_fields.named,
syn::Fields::Unnamed(unnamed_fields) => &unnamed_fields.unnamed,
_ => return Err(Error::new(self.span(), "only named field allowed")),
_ => return syn_error!(self.span(), "only named fields are allowed in structs"),
};
let fields = fields
.iter()
.map(|field| {
check_field(field)?;
let field_name = field.ident.as_ref().map(|ident| {
ident
.to_string()
.split(' ')
.last()
.unwrap_or_default()
.to_string()
});
let field_type = ParsedType::from_type(&field.ty)?;
Ok(AstRecordField {
name: field_name,
ty: field_type,
})
})
.collect::<Result<Vec<_>>>()?;
let fields = fields_into_ast(fields)?;
let name = self.ident.to_string();
let ast_record_item = fce_ast_types::AstRecordItem {
@ -69,15 +51,38 @@ fn check_record(record: &syn::ItemStruct) -> Result<()> {
|| record.generics.gt_token.is_some()
|| record.generics.where_clause.is_some()
{
return Err(Error::new(
return syn_error!(
record.span(),
"#[fce] couldn't be applied to a struct with generics or lifetimes",
));
"#[fce] couldn't be applied to a struct with generics or lifetimes"
);
}
Ok(())
}
fn fields_into_ast(
fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
) -> Result<Vec<AstRecordField>> {
fields
.iter()
.map(|field| {
check_field(field)?;
let name = field.ident.as_ref().map(|ident| {
ident
.to_string()
.split(' ')
.last()
.unwrap_or_default()
.to_string()
});
let ty = ParsedType::from_type(&field.ty)?;
let record_field = AstRecordField { name, ty };
Ok(record_field)
})
.collect::<Result<Vec<_>>>()
}
/// Check that record fields satisfy the following requirements:
/// - all fields must be public
/// - field must have only doc attributes
@ -85,10 +90,10 @@ fn check_field(field: &syn::Field) -> Result<()> {
match field.vis {
syn::Visibility::Public(_) => {}
_ => {
return Err(Error::new(
return syn_error!(
field.span(),
"#[fce] could be applied only to struct with all public fields",
))
"#[fce] could be applied only to struct with all public fields"
)
}
};
@ -104,7 +109,7 @@ fn check_field(field: &syn::Field) -> Result<()> {
});
if !is_all_attrs_public {
return Err(Error::new(field.span(), "field attributes isn't allowed"));
return syn_error!(field.span(), "field attributes isn't allowed");
}
Ok(())

View File

@ -15,6 +15,7 @@
*/
use super::ParsedType;
use crate::fce_ast_types::AstFuncArgument;
use crate::wasm_type::RustType;
/// This trait could be used to generate raw args needed to construct a export function.
@ -22,9 +23,9 @@ pub(crate) trait FnArgGlueCodeGenerator {
fn generate_arguments(&self) -> Vec<RustType>;
}
impl FnArgGlueCodeGenerator for (String, ParsedType) {
impl FnArgGlueCodeGenerator for AstFuncArgument {
fn generate_arguments(&self) -> Vec<RustType> {
match self.1 {
match self.ty {
ParsedType::Boolean(_) => vec![RustType::I32],
ParsedType::I8(_) => vec![RustType::I8],
ParsedType::I16(_) => vec![RustType::I16],

View File

@ -18,6 +18,7 @@ use super::ParsedType;
use super::passing_style_of;
use super::PassingStyle;
use crate::new_ident;
use crate::fce_ast_types::AstFuncArgument;
use quote::quote;
@ -29,6 +30,13 @@ pub(crate) struct FnEpilogDescriptor {
pub(crate) mem_forget: proc_macro2::TokenStream,
}
/// Contains all ingredients needed for epilog creation.
pub(crate) struct FnEpilogIngredients<'i> {
pub(crate) args: &'i [AstFuncArgument],
pub(crate) converted_args: &'i [syn::Ident],
pub(crate) return_type: &'i Option<ParsedType>,
}
/// This trait could be used to generate various parts needed to construct epilog of an export
/// function. They are marked with # in the following example:
/// ```ignore
@ -44,24 +52,18 @@ pub(crate) trait FnEpilogGlueCodeGenerator {
fn generate_fn_epilog(&self) -> FnEpilogDescriptor;
}
impl FnEpilogGlueCodeGenerator
for (
&Vec<(String, ParsedType)>,
&Vec<syn::Ident>,
&Option<ParsedType>,
)
{
impl FnEpilogGlueCodeGenerator for FnEpilogIngredients<'_> {
fn generate_fn_epilog(&self) -> FnEpilogDescriptor {
FnEpilogDescriptor {
fn_return_type: generate_fn_return_type(self.2),
return_expression: generate_return_expression(self.2),
epilog: generate_epilog(self.2),
mem_forget: generate_mem_forget(self.0, self.1, self.2),
fn_return_type: generate_fn_return_type(self.return_type),
return_expression: generate_return_expression(self.return_type),
epilog: generate_epilog(self.return_type),
mem_forget: generate_mem_forgets(self),
}
}
}
fn generate_fn_return_type(ty: &Option<ParsedType>) -> proc_macro2::TokenStream {
pub(crate) fn generate_fn_return_type(ty: &Option<ParsedType>) -> proc_macro2::TokenStream {
let ty = match ty {
Some(ParsedType::Boolean(_)) => Some("i32"),
Some(ParsedType::I8(_)) => Some("i8"),
@ -90,7 +92,7 @@ fn generate_fn_return_type(ty: &Option<ParsedType>) -> proc_macro2::TokenStream
}
}
fn generate_return_expression(ty: &Option<ParsedType>) -> proc_macro2::TokenStream {
pub(crate) fn generate_return_expression(ty: &Option<ParsedType>) -> proc_macro2::TokenStream {
match ty {
None => quote! {},
_ => quote! {
@ -115,10 +117,10 @@ fn generate_epilog(ty: &Option<ParsedType>) -> proc_macro2::TokenStream {
}
}
Some(ParsedType::Vector(ty, _)) => {
let generated_serializer_name = String::from("__fce_generated_vec_serializer");
let generated_serializer_name = "__fce_generated_vec_serializer";
let generated_serializer_ident = new_ident!(generated_serializer_name);
let vector_serializer =
super::vector_utils::generate_vector_serializer(ty, &generated_serializer_name);
super::vector_utils::generate_vector_serializer(ty, generated_serializer_name);
quote! {
#vector_serializer
@ -136,12 +138,8 @@ fn generate_epilog(ty: &Option<ParsedType>) -> proc_macro2::TokenStream {
/// If an export function returns a reference, this is probably a reference to one
/// of the function arguments. If that's the case, reference must be still valid after
/// the end of the function. Their deletion will be handled by IT with calling `release_objects`.
fn generate_mem_forget(
args: &Vec<(String, ParsedType)>,
converted_args: &Vec<syn::Ident>,
ret_type: &Option<ParsedType>,
) -> proc_macro2::TokenStream {
let passing_style = ret_type.as_ref().map(passing_style_of);
fn generate_mem_forgets(ingredients: &FnEpilogIngredients<'_>) -> proc_macro2::TokenStream {
let passing_style = ingredients.return_type.as_ref().map(passing_style_of);
match passing_style {
// result will be deleted by IT side
@ -149,21 +147,21 @@ fn generate_mem_forget(
quote! { fluence::internal::add_object_to_release(Box::new(result)); }
}
Some(PassingStyle::ByRef) | Some(PassingStyle::ByMutRef) => {
mem_forget_by_args(args, converted_args)
mem_forget_by_args(ingredients.args, ingredients.converted_args)
}
None => quote! {},
}
}
fn mem_forget_by_args(
args: &Vec<(String, ParsedType)>,
converted_args: &Vec<syn::Ident>,
args: &[AstFuncArgument],
converted_args: &[syn::Ident],
) -> proc_macro2::TokenStream {
debug_assert!(args.len() == converted_args.len());
let mut res = proc_macro2::TokenStream::new();
for ((_, arg), converted_arg) in args.iter().zip(converted_args) {
let arg_passing_style = passing_style_of(arg);
for (arg, converted_arg) in args.iter().zip(converted_args) {
let arg_passing_style = passing_style_of(&arg.ty);
match arg_passing_style {
// such values will be deleted inside an export function because they are being moved
PassingStyle::ByValue => {}

View File

@ -19,6 +19,7 @@ use super::FnArgGlueCodeGenerator;
use super::passing_style_of;
use crate::new_ident;
use crate::wasm_type::RustType;
use crate::fce_ast_types::AstFuncArgument;
use crate::parsed_type::PassingStyle;
use quote::quote;
@ -47,7 +48,7 @@ pub(crate) trait FnPrologGlueCodeGenerator {
fn generate_prolog(&self) -> FnPrologDescriptor;
}
impl FnPrologGlueCodeGenerator for Vec<(String, ParsedType)> {
impl FnPrologGlueCodeGenerator for Vec<AstFuncArgument> {
fn generate_prolog(&self) -> FnPrologDescriptor {
let mut raw_arg_names = Vec::with_capacity(self.len());
let mut raw_arg_types = Vec::with_capacity(self.len());
@ -57,11 +58,11 @@ impl FnPrologGlueCodeGenerator for Vec<(String, ParsedType)> {
let mut input_type_id = 0;
for arg in self {
let passing_style = passing_style_of(&arg.1);
let passing_style = passing_style_of(&arg.ty);
let TypeLifter {
converted_arg_ident,
type_lifter_glue_code,
} = generate_type_lifting_prolog(&arg.1, passing_style, input_type_id, input_type_id);
} = generate_type_lifting_prolog(&arg.ty, passing_style, input_type_id, input_type_id);
let curr_raw_arg_types = arg.generate_arguments();
let arg = quote! { #passing_style #converted_arg_ident };

View File

@ -57,11 +57,11 @@ impl ForeignModEpilogGlueCodeGenerator for Option<ParsedType> {
)
},
Some(ParsedType::Vector(ty, _)) => {
let generated_deserializer_name = String::from("__fce_generated_vec_deserializer");
let generated_deserializer_name = "__fce_generated_vec_deserializer";
let generated_deserializer_ident = new_ident!(generated_deserializer_name);
let vector_deserializer = super::vector_utils::generate_vector_deserializer(
ty,
&generated_deserializer_name,
generated_deserializer_name,
);
quote! {

View File

@ -18,6 +18,7 @@ use super::ParsedType;
use crate::wasm_type::RustType;
use crate::new_ident;
use crate::parsed_type::PassingStyle;
use crate::fce_ast_types::AstFuncArgument;
pub(crate) struct WrapperDescriptor {
pub(crate) arg_names: Vec<syn::Ident>,
@ -58,32 +59,33 @@ pub(crate) trait ForeignModPrologGlueCodeGenerator {
fn generate_extern_prolog(&self) -> ExternDescriptor;
}
impl ForeignModPrologGlueCodeGenerator for Vec<(String, ParsedType)> {
impl ForeignModPrologGlueCodeGenerator for Vec<AstFuncArgument> {
fn generate_wrapper_prolog(&self) -> WrapperDescriptor {
use crate::parsed_type::foreign_mod_arg::ForeignModArgGlueCodeGenerator;
use quote::ToTokens;
let arg_types: Vec<proc_macro2::TokenStream> = self
.iter()
.map(|(_, input_type)| input_type.to_token_stream())
.map(|arg| arg.ty.to_token_stream())
.collect();
let (arg_names, arg_transforms, arg_drops) = self
.iter()
.enumerate()
.fold((Vec::new(), proc_macro2::TokenStream::new(), proc_macro2::TokenStream::new()), |(mut arg_names, mut arg_transforms, mut arg_drops), (id, (_, ty))| {
.fold((Vec::new(), proc_macro2::TokenStream::new(), proc_macro2::TokenStream::new()), |(mut arg_names, mut arg_transforms, mut arg_drops), (id, arg)| {
let arg_name = format!("arg_{}", id);
let arg_ident = new_ident!(arg_name);
arg_names.push(arg_ident.clone());
// arguments of following two types shouldn't be deleted after transformation to raw view
match ty {
match &arg.ty {
ParsedType::Utf8String(PassingStyle::ByValue) => {
arg_transforms.extend(quote::quote! { let mut #arg_ident = std::mem::ManuallyDrop::new(#arg_ident); });
arg_drops.extend(quote::quote! { std::mem::ManuallyDrop::drop(&mut #arg_ident); });
},
ParsedType::Vector(ty, _) => {
let generated_serializer_name = format!("__fce_generated_vec_serializer_{}", arg_name);
let generated_serializer_ident = new_ident!(generated_serializer_name);
let vector_serializer = super::vector_utils::generate_vector_serializer(ty, &generated_serializer_name);
@ -104,7 +106,7 @@ impl ForeignModPrologGlueCodeGenerator for Vec<(String, ParsedType)> {
let raw_args: Vec<proc_macro2::TokenStream> = self
.iter()
.enumerate()
.map(|(id, (_, input_type))| input_type.generate_raw_args(id))
.map(|(id, arg)| arg.ty.generate_raw_args(id))
.collect();
WrapperDescriptor {

View File

@ -17,8 +17,10 @@
use crate::fce_ast_types;
use crate::parsed_type::FnEpilogGlueCodeGenerator;
use crate::parsed_type::FnEpilogDescriptor;
use crate::parsed_type::FnEpilogIngredients;
use crate::parsed_type::FnPrologGlueCodeGenerator;
use crate::parsed_type::FnPrologDescriptor;
use crate::new_ident;
use proc_macro2::TokenStream;
@ -52,17 +54,18 @@ impl quote::ToTokens for fce_ast_types::AstFunctionItem {
args,
} = &signature.arguments.generate_prolog();
let epilog_ingredients = FnEpilogIngredients {
args: &signature.arguments,
converted_args: converted_arg_idents,
return_type: &signature.output_type,
};
let FnEpilogDescriptor {
fn_return_type,
return_expression,
epilog,
mem_forget,
} = (
&signature.arguments,
converted_arg_idents,
&signature.output_type,
)
.generate_fn_epilog();
} = epilog_ingredients.generate_fn_epilog();
// here this Option must be Some
let original_func = &self.original;

View File

@ -42,7 +42,7 @@ impl quote::ToTokens for fce_ast_types::AstExternModItem {
#[link(wasm_import_module = #wasm_import_module_name)]
#[cfg(target_arch = "wasm32")]
extern "C" {
#generated_imports
#(#generated_imports)*
}
#[cfg(not(target_arch = "wasm32"))]
@ -61,15 +61,14 @@ impl quote::ToTokens for fce_ast_types::AstExternModItem {
}
}
fn generate_extern_section_items(extern_item: &fce_ast_types::AstExternModItem) -> TokenStream {
let mut token_stream = TokenStream::new();
fn generate_extern_section_items(
extern_item: &fce_ast_types::AstExternModItem,
) -> Vec<TokenStream> {
let mut section_items = Vec::with_capacity(extern_item.imports.len());
for import in &extern_item.imports {
let signature = &import.signature;
let FnEpilogDescriptor { fn_return_type, .. } =
(&vec![], &vec![], &signature.output_type).generate_fn_epilog();
let fn_return_type = crate::parsed_type::generate_fn_return_type(&signature.output_type);
let link_name = import.link_name.as_ref().unwrap_or(&signature.name);
let import_name = generate_import_name(&signature.name);
let ExternDescriptor {
@ -82,10 +81,10 @@ fn generate_extern_section_items(extern_item: &fce_ast_types::AstExternModItem)
fn #import_name(#(#raw_arg_names: #raw_arg_types),*) #fn_return_type;
};
token_stream.extend(func);
section_items.push(func);
}
token_stream
section_items
}
#[rustfmt::skip]
@ -113,10 +112,8 @@ fn generate_wrapper_functions(extern_item: &fce_ast_types::AstExternModItem) ->
arg_drops,
} = signature.arguments.generate_wrapper_prolog();
let FnEpilogDescriptor {
return_expression, ..
} = (&vec![], &vec![], &signature.output_type).generate_fn_epilog();
let return_expression =
crate::parsed_type::generate_return_expression(&signature.output_type);
let epilog = signature.output_type.generate_wrapper_epilog();
let wrapper_func = quote! {

View File

@ -50,6 +50,7 @@ impl RecordSerializerGlueCodeGenerator for fce_ast_types::AstRecordItem {
field.name.as_ref().unwrap(),
id
);
let generated_serializer_ident = new_ident!(generated_serializer_name);
let vector_serializer = crate::parsed_type::generate_vector_serializer(
ty,

View File

@ -44,6 +44,13 @@ macro_rules! prepare_global_data {
};
}
#[macro_export]
macro_rules! syn_error {
($span:expr, $message:expr) => {
Err(syn::Error::new($span, $message))
};
}
/// Calculate record size in an internal serialized view.
pub fn get_record_size<'a>(
fields: impl Iterator<Item = &'a crate::parsed_type::ParsedType>,

View File

@ -16,7 +16,7 @@ error: types with lifetimes or generics aren't allowed
14 | fn test3(_arg_1: std::collections::HashMap<i32, String>) {}
| ^^^^^^^^^^^^^^^^^^^^
error: Incorrect argument type - passing only by value is supported now
error: Incorrect argument type, only path or reference are available on this position
--> $DIR/improper_types.rs:17:26
|
17 | fn test4(_arg_1: i32) -> (i32, i32) {

View File

@ -0,0 +1,8 @@
#![allow(improper_ctypes)]
use fluence::fce;
fn main() {}
#[fce]
struct A(pub i32, pub u32);

View File

@ -0,0 +1,5 @@
error: only named fields are allowed in structs
--> $DIR/unnamed_structs.rs:8:1
|
8 | struct A(pub i32, pub u32);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -13,4 +13,5 @@ fn test() {
tests.pass("tests/records/empty_struct.rs");
tests.compile_fail("tests/records/struct_with_improper_types.rs");
tests.compile_fail("tests/records/struct_with_private_fields.rs");
tests.compile_fail("tests/records/unnamed_structs.rs");
}