Skip to content

Commit

Permalink
Construct external Rust types with named fields (#589)
Browse files Browse the repository at this point in the history
Co-authored-by: Joshua Dutton <[email protected]>
Co-authored-by: John-John Tedro <[email protected]>
  • Loading branch information
3 people authored Jul 30, 2023
1 parent 013dcd8 commit 35dded3
Show file tree
Hide file tree
Showing 17 changed files with 481 additions and 40 deletions.
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
.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

0 comments on commit 35dded3

Please sign in to comment.