Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Construct external Rust types with named fields #589

Merged
merged 13 commits into from
Jul 30, 2023
24 changes: 22 additions & 2 deletions crates/rune-macros/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ pub(crate) fn expand_install_with(

match &input.data {
syn::Data::Struct(st) => {
expand_struct_install_with(cx, installers, st, tokens, attr)?;
expand_struct_install_with(cx, installers, ident, st, tokens, attr)?;
}
syn::Data::Enum(en) => {
expand_enum_install_with(cx, installers, ident, en, tokens, attr, generics)?;
Expand All @@ -164,6 +164,7 @@ pub(crate) fn expand_install_with(
fn expand_struct_install_with(
cx: &Context,
installers: &mut Vec<TokenStream>,
ident: &syn::Ident,
st: &syn::DataStruct,
tokens: &Tokens,
attr: &TypeAttr,
Expand Down Expand Up @@ -217,13 +218,32 @@ fn expand_struct_install_with(

match &st.fields {
syn::Fields::Named(fields) => {
let constructor = attr
.constructor
udoprog marked this conversation as resolved.
Show resolved Hide resolved
.then(|| {
let args = fields.named.iter().map(|f| {
let ident = f.ident.as_ref().expect("named fields must have an Ident");
let typ = &f.ty;
quote!(#ident: #typ)
});

let field_names = fields.named.iter().map(|f| f.ident.as_ref());

quote!(|#(#args),*| {
#ident {
#(#field_names),*
}
})
})
.map(|c| quote!(.constructor(#c)?));

let fields = fields.named.iter().flat_map(|f| {
let ident = f.ident.as_ref()?;
Some(syn::LitStr::new(&ident.to_string(), ident.span()))
});

installers.push(quote! {
module.type_meta::<Self>()?.make_named_struct(&[#(#fields,)*])?.static_docs(&#docs);
module.type_meta::<Self>()?.make_named_struct(&[#(#fields,)*])?#constructor.static_docs(&#docs);
});
}
syn::Fields::Unnamed(fields) => {
Expand Down
4 changes: 4 additions & 0 deletions crates/rune-macros/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub(crate) struct TypeAttr {
pub(crate) parse: ParseKind,
/// `#[rune(item = <path>)]`.
pub(crate) item: Option<syn::Path>,
/// `#[rune(constructor)]`.
pub(crate) constructor: bool,
/// Parsed documentation.
pub(crate) docs: Vec<syn::Expr>,
}
Expand Down Expand Up @@ -441,6 +443,8 @@ impl Context {
// Parse `#[rune(install_with = <path>)]`
meta.input.parse::<Token![=]>()?;
attr.install_with = Some(parse_path_compat(meta.input)?);
} else if meta.path == CONSTRUCTOR {
attr.constructor = true;
} else {
return Err(syn::Error::new_spanned(
&meta.path,
Expand Down
73 changes: 60 additions & 13 deletions crates/rune/src/compile/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,17 +429,51 @@ impl Context {

let kind = if let Some(spec) = &ty.spec {
match spec {
TypeSpecification::Struct(fields) => meta::Kind::Struct {
fields: match fields {
Fields::Named(fields) => meta::Fields::Named(meta::FieldsNamed {
fields: fields.iter().copied().map(Box::<str>::from).collect(),
}),
Fields::Unnamed(args) => meta::Fields::Unnamed(*args),
Fields::Empty => meta::Fields::Empty,
},
constructor: None,
parameters,
},
TypeSpecification::Struct(fields) => {
let constructor = match &ty.constructor {
Some(c) => {
let hash = Hash::type_hash(&item);

let signature = meta::Signature {
#[cfg(feature = "doc")]
is_async: false,
#[cfg(feature = "doc")]
args: Some(match fields {
Fields::Named(names) => names.len(),
Fields::Unnamed(args) => *args,
Fields::Empty => 0,
}),
#[cfg(feature = "doc")]
return_type: Some(ty.hash),
#[cfg(feature = "doc")]
argument_types: Box::from([]),
};

self.insert_native_fn(hash, c)?;
Some(signature)
}
None => None,
};

meta::Kind::Struct {
fields: match fields {
Fields::Named(fields) => meta::Fields::Named(meta::FieldsNamed {
fields: fields
.iter()
.copied()
.enumerate()
.map(|(position, name)| {
(Box::<str>::from(name), meta::FieldMeta { position })
})
.collect(),
}),
Fields::Unnamed(args) => meta::Fields::Unnamed(*args),
Fields::Empty => meta::Fields::Empty,
},
constructor,
parameters,
}
}
TypeSpecification::Enum(en) => {
for (index, variant) in en.variants.iter().enumerate() {
let Some(fields) = &variant.fields else {
Expand Down Expand Up @@ -495,7 +529,13 @@ impl Context {
fields: names
.iter()
.copied()
.map(Box::<str>::from)
.enumerate()
.map(|(position, name)| {
(
Box::<str>::from(name),
meta::FieldMeta { position },
)
})
.collect(),
})
}
Expand Down Expand Up @@ -874,7 +914,14 @@ impl Context {
index,
fields: match fields {
Fields::Named(fields) => meta::Fields::Named(meta::FieldsNamed {
fields: fields.iter().copied().map(Box::<str>::from).collect(),
fields: fields
.iter()
.copied()
.enumerate()
.map(|(position, name)| {
(Box::<str>::from(name), meta::FieldMeta { position })
})
.collect(),
}),
Fields::Unnamed(args) => meta::Fields::Unnamed(*args),
Fields::Empty => meta::Fields::Empty,
Expand Down
9 changes: 9 additions & 0 deletions crates/rune/src/compile/context_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ pub enum ContextError {
ConflictingVariant {
item: ItemBuf,
},
ConstructorConflict {
type_info: TypeInfo,
},
ValueError {
error: VmError,
},
Expand Down Expand Up @@ -196,6 +199,12 @@ impl fmt::Display for ContextError {
ContextError::ConflictingVariant { item } => {
write!(f, "Variant with `{item}` already exists")?;
}
ContextError::ConstructorConflict { type_info } => {
write!(
f,
"Constructor for type `{type_info}` has already been registered"
)?;
}
ContextError::ValueError { error } => {
write!(f, "Error when converting to constant value: {error}")?;
}
Expand Down
11 changes: 9 additions & 2 deletions crates/rune/src/compile/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use core::fmt;

use crate::no_std::borrow::Cow;
use crate::no_std::collections::HashSet;
use crate::no_std::collections::HashMap;
use crate::no_std::path::Path;
use crate::no_std::prelude::*;

Expand Down Expand Up @@ -287,7 +287,14 @@ pub struct Import {
#[non_exhaustive]
pub struct FieldsNamed {
/// Fields associated with the type.
pub(crate) fields: HashSet<Box<str>>,
pub(crate) fields: HashMap<Box<str>, FieldMeta>,
}

/// Metadata for a single named field.
#[derive(Debug, Clone)]
pub struct FieldMeta {
/// Position of the field in its containing type declaration.
pub(crate) position: usize,
}

/// Item and the module that the item belongs to.
Expand Down
58 changes: 57 additions & 1 deletion crates/rune/src/compile/v1/assemble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1957,7 +1957,9 @@ fn expr_object<'hir>(
) -> compile::Result<Asm<'hir>> {
let guard = cx.scopes.child(span)?;

for assign in hir.assignments {
let base = cx.scopes.total(span)?;

for assign in hir.assignments.iter() {
expr(cx, &assign.assign, Needs::Value)?.apply(cx)?;
cx.scopes.alloc(&span)?;
}
Expand All @@ -1976,6 +1978,10 @@ fn expr_object<'hir>(
hir::ExprObjectKind::StructVariant { hash } => {
cx.asm.push(Inst::StructVariant { hash, slot }, span);
}
hir::ExprObjectKind::ExternalType { hash, args } => {
reorder_field_assignments(cx, hir, base, span)?;
cx.asm.push(Inst::Call { hash, args }, span);
}
hir::ExprObjectKind::Anonymous => {
cx.asm.push(Inst::Object { slot }, span);
}
Expand All @@ -1991,6 +1997,56 @@ fn expr_object<'hir>(
Ok(Asm::top(span))
}

/// Reorder the position of the field assignments on the stack so that they
/// match the expected argument order when invoking the constructor function.
fn reorder_field_assignments<'hir>(
cx: &mut Ctxt<'_, 'hir, '_>,
hir: &hir::ExprObject<'hir>,
base: usize,
span: &dyn Spanned,
) -> compile::Result<()> {
let mut order = Vec::with_capacity(hir.assignments.len());

for assign in hir.assignments {
let Some(position) = assign.position else {
return Err(compile::Error::msg(
span,
format_args!("Missing position for field assignment {}", assign.key.1),
));
};

order.push(position);
}

for a in 0..hir.assignments.len() {
loop {
let Some(&b) = order.get(a) else {
return Err(compile::Error::msg(
span,
"Order out-of-bounds",
));
};

if a == b {
break;
}

order.swap(a, b);

let (Some(a), Some(b)) = (base.checked_add(a), base.checked_add(b)) else {
return Err(compile::Error::msg(
span,
"Field repositioning out-of-bounds",
));
};

cx.asm.push(Inst::Swap { a, b }, span);
}
}

Ok(())
}

/// Assemble a range expression.
#[instrument(span = span)]
fn expr_range<'hir>(
Expand Down
3 changes: 3 additions & 0 deletions crates/rune/src/hir/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ pub(crate) enum ExprObjectKind {
UnitStruct { hash: Hash },
Struct { hash: Hash },
StructVariant { hash: Hash },
ExternalType { hash: Hash, args: usize },
Anonymous,
}

Expand All @@ -601,6 +602,8 @@ pub(crate) struct FieldAssign<'hir> {
pub(crate) key: (Span, &'hir str),
/// The assigned expression of the field.
pub(crate) assign: Expr<'hir>,
/// The position of the field in its containing type declaration.
pub(crate) position: Option<usize>,
}

/// A literal vector.
Expand Down
46 changes: 30 additions & 16 deletions crates/rune/src/hir/lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ pub(crate) fn expr_object<'hir>(
let span = ast;
let mut keys_dup = HashMap::new();

let assignments = &*iter!(&ast.assignments, |(ast, _)| {
let assignments = &mut *iter!(&ast.assignments, |(ast, _)| {
let key = object_key(cx, &ast.key)?;

if let Some(existing) = keys_dup.insert(key.1, key.0) {
Expand Down Expand Up @@ -362,25 +362,31 @@ pub(crate) fn expr_object<'hir>(
hir::FieldAssign {
key: (key.0.span(), key.1),
assign,
position: None,
}
});

let check_object_fields = |fields: &HashSet<_>, item: &Item| {
let mut check_object_fields = |fields: &HashMap<_, meta::FieldMeta>, item: &Item| {
let mut fields = fields.clone();

for assign in assignments {
if !fields.remove(assign.key.1) {
return Err(compile::Error::new(
assign.key.0,
ErrorKind::LitObjectNotField {
field: assign.key.1.into(),
item: item.to_owned(),
},
));
}
for assign in assignments.iter_mut() {
match fields.remove(assign.key.1) {
Some(field_meta) => {
assign.position = Some(field_meta.position);
}
None => {
return Err(compile::Error::new(
assign.key.0,
ErrorKind::LitObjectNotField {
field: assign.key.1.into(),
item: item.to_owned(),
},
));
}
};
}

if let Some(field) = fields.into_iter().next() {
if let Some(field) = fields.into_keys().next() {
return Err(compile::Error::new(
span,
ErrorKind::LitObjectMissingField {
Expand All @@ -405,15 +411,23 @@ pub(crate) fn expr_object<'hir>(
fields: meta::Fields::Empty,
..
} => {
check_object_fields(&HashSet::new(), item)?;
check_object_fields(&HashMap::new(), item)?;
hir::ExprObjectKind::UnitStruct { hash: meta.hash }
}
meta::Kind::Struct {
fields: meta::Fields::Named(st),
constructor,
..
} => {
check_object_fields(&st.fields, item)?;
hir::ExprObjectKind::Struct { hash: meta.hash }

match constructor {
Some(_) => hir::ExprObjectKind::ExternalType {
hash: meta.hash,
args: st.fields.len(),
},
None => hir::ExprObjectKind::Struct { hash: meta.hash },
}
}
meta::Kind::Variant {
fields: meta::Fields::Named(st),
Expand Down Expand Up @@ -1167,7 +1181,7 @@ fn pat<'hir>(cx: &mut Ctxt<'hir, '_, '_>, ast: &ast::Pat) -> compile::Result<hir
));
};

let mut fields = st.fields.clone();
let mut fields: HashSet<_> = st.fields.keys().cloned().collect();

for binding in bindings.iter() {
if !fields.remove(binding.key()) {
Expand Down
Loading