Source code
Revision control
Copy as Markdown
Other Tools
//! #[diplomat::attr] and other attributes
use crate::ast;
use crate::ast::attrs::{AttrInheritContext, DiplomatBackendAttrCfg, StandardAttribute};
use crate::hir::lowering::ErrorStore;
use crate::hir::{
EnumVariant, LoweringError, Method, Mutability, OpaqueId, ReturnType, SelfType, SuccessType,
TraitDef, Type, TypeDef, TypeId,
};
use syn::Meta;
pub use crate::ast::attrs::RenameAttr;
/// Diplomat attribute that can be specified on items, methods, and enum variants. These
/// can be used to control the codegen in a particular backend.
///
/// Most of these are specified via `#[diplomat::attr(some cfg here, attrname)]`, where `some cfg here`
/// can be used to pick which backends something applies to.
#[non_exhaustive]
#[derive(Clone, Default, Debug)]
pub struct Attrs {
/// "disable" this item: do not generate code for it in the backend
///
/// This attribute is always inherited except to variants
pub disable: bool,
/// An optional namespace. None is equivalent to the root namespace.
///
/// This attribute is inherited to types (and is not allowed elsewhere)
pub namespace: Option<String>,
/// Rename this item/method/variant
///
/// This attribute is inherited except through methods and variants (and is not allowed on variants)
pub rename: RenameAttr,
/// Rename this item in the C ABI. This *must* be respected by backends.
///
/// This attribute is inherited except through variants
pub abi_rename: RenameAttr,
/// This method is "special": it should generate something other than a regular method on the other side.
/// This can be something like a constructor, an accessor, a stringifier etc.
///
/// This attribute does not participate in inheritance and must always
/// be specified on individual methods
pub special_method: Option<SpecialMethod>,
/// This user-defined type can be used as the error type in a Result.
pub custom_errors: bool,
/// From #[diplomat::demo()]. Created from [`crate::ast::attrs::Attrs::demo_attrs`].
/// List of attributes specific to automatic demo generation.
/// Currently just for demo_gen in diplomat-tool (which generates sample webpages), but could be used for broader purposes (i.e., demo Android apps)
pub demo_attrs: DemoInfo,
}
// #region: Demo specific attributes.
/// For `#[diplomat::demo(input(...))]`, stored in [DemoInfo::input_cfg].
#[non_exhaustive]
#[derive(Clone, Default, Debug)]
pub struct DemoInputCFG {
/// `#[diplomat::demo(input(label = "..."))]`
/// Label that this input parameter should have. Let demo_gen pick a valid name if this is empty.
///
/// For instance <label for="v">Number Here</label><input name="v"/>
pub label: String,
/// `#[diplomat::demo(input(default_value = "..."))]`
/// Sets the default value for a parameter.
///
/// Should ALWAYS be a string. The HTML renderer is expected to do validation for us.
pub default_value: String,
}
#[non_exhaustive]
#[derive(Clone, Default, Debug)]
pub struct DemoInfo {
/// `#[diplomat::demo(generate)]`. If automatic generation is disabled by default (see [`diplomat_tool::demo_gen::DemoConfig`]), then the below render terminus will be allowed to generate.
pub generate: bool,
/// `#[diplomat::demo(default_constructor)]`
/// We search for any methods specially tagged with `Constructor`, but if there's are no default Constructors and there's NamedConstructor that you want to be default instead, use this.
/// TODO: Should probably ignore other `Constructors` if a default has been set.
pub default_constructor: bool,
/// `#[diplomat::demo(external)]` represents an item that we will not evaluate, and should be passed to the rendering engine to provide.
pub external: bool,
/// `#[diplomat::demo(custom_func = "/file/name/here.mjs")]` can be used above any `struct` definition in the bridge. The linked `.mjs` should contain a JS definition of functions that should be bundled with demo_gen's output.
///
/// We call these functions "custom functions", as they are JS functions that are not automagically generated by demo_gen, but rather included as part of its JS output in the `RenderInfo` object.
///
/// For more information on custom functions (and their use), see the relevant chapter in [the book](https://rust-diplomat.github.io/diplomat/demo_gen/custom_functions.html).
///
/// Files are located relative to lib.rs.
///
pub custom_func: Option<String>,
/// `#[diplomat::demo(input(...))]` represents configuration options for anywhere we might expect user input.
pub input_cfg: DemoInputCFG,
}
// #endregion
/// Attributes that mark methods as "special"
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum SpecialMethod {
/// A constructor.
///
/// Must return Self (or Result<Self> for backends with `fallible_constructors` enabled )
Constructor,
/// A named constructor, with optional name. If the name isn't specified, it will be derived
/// from the method name
///
/// Must return Self (or Result<Self> for backends with `fallible_constructors` enabled )
NamedConstructor(Option<String>),
/// A getter, with optional name. If the name isn't specified, it will be derived
/// from the method name
///
/// Must have no parameters and must return something.
Getter(Option<String>),
/// A setter, with optional name. If the name isn't specified, it will be derived
/// from the method name
///
/// Must have no return type (aside from potentially a `Result<(), _>`) and must have one parameter
Setter(Option<String>),
/// A stringifier. Must have no parameters and return a string (DiplomatWrite)
Stringifier,
/// A comparison operator. Currently not universally supported
Comparison,
/// An iterator (a type that is mutated to produce new values)
Iterator,
/// An iterable (a type that can produce an iterator)
Iterable,
/// Indexes into the type using an integer
Indexer,
/// Arithmetic operators. May not return references
Add,
Sub,
Mul,
Div,
/// In-place arithmetic operators. Must not return a value
AddAssign,
SubAssign,
MulAssign,
DivAssign,
}
impl SpecialMethod {
pub fn from_path_and_meta(path: &str, meta: &Meta) -> Result<Option<Self>, LoweringError> {
let parse_meta = |meta| match StandardAttribute::from_meta(meta) {
Ok(StandardAttribute::String(s)) => Ok(Some(s)),
Ok(StandardAttribute::Empty) => Ok(None),
Ok(_) | Err(_) => Err(LoweringError::Other(format!(
"`{path}` must have a single string parameter or no parameter"
))),
};
match path {
"constructor" => Ok(Some(Self::Constructor)),
"named_constructor" => Ok(Some(Self::NamedConstructor(parse_meta(meta)?))),
"getter" => Ok(Some(Self::Getter(parse_meta(meta)?))),
"setter" => Ok(Some(Self::Setter(parse_meta(meta)?))),
"stringifier" => Ok(Some(Self::Stringifier)),
"comparison" => Ok(Some(Self::Comparison)),
"iterator" => Ok(Some(Self::Iterator)),
"iterable" => Ok(Some(Self::Iterable)),
"indexer" => Ok(Some(Self::Indexer)),
"add" => Ok(Some(Self::Add)),
"sub" => Ok(Some(Self::Sub)),
"mul" => Ok(Some(Self::Mul)),
"div" => Ok(Some(Self::Div)),
"add_assign" => Ok(Some(Self::AddAssign)),
"sub_assign" => Ok(Some(Self::SubAssign)),
"mul_assign" => Ok(Some(Self::MulAssign)),
"div_assign" => Ok(Some(Self::DivAssign)),
_ => Ok(None),
}
}
// Returns the standard operator string (if any) associated with this special method
pub fn operator_str(&self) -> Option<&str> {
match self {
SpecialMethod::Add => Some("+"),
SpecialMethod::Sub => Some("-"),
SpecialMethod::Mul => Some("*"),
SpecialMethod::Div => Some("/"),
SpecialMethod::AddAssign => Some("+="),
SpecialMethod::SubAssign => Some("-="),
SpecialMethod::MulAssign => Some("*="),
SpecialMethod::DivAssign => Some("/="),
SpecialMethod::Indexer => Some("[]"),
_ => None,
}
}
}
/// For special methods that affect type semantics, whether this type has this method.
///
/// This will likely only contain a subset of special methods, but feel free to add more as needed.
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct SpecialMethodPresence {
pub comparator: bool,
/// If it is an iterator, the type it iterates over
pub iterator: Option<SuccessType>,
/// If it is an iterable, the iterator type it returns (*not* the type it iterates over,
/// perform lookup on that type to access)
pub iterable: Option<OpaqueId>,
}
/// Where the attribute was found. Some attributes are only allowed in some contexts
/// (e.g. namespaces cannot be specified on methods)
#[non_exhaustive] // might add module attrs in the future
#[derive(Debug)]
pub enum AttributeContext<'a, 'b> {
Type(TypeDef<'a>),
Trait(&'a TraitDef),
EnumVariant(&'a EnumVariant),
Method(&'a Method, TypeId, &'b mut SpecialMethodPresence),
Module,
Param,
SelfParam,
Field,
}
fn maybe_error_unsupported(
auto_found: bool,
attribute: &str,
backend: &str,
errors: &mut ErrorStore,
) {
if !auto_found {
errors.push(LoweringError::Other(format!(
"`{attribute}` not supported in backend {backend}"
)));
}
}
impl Attrs {
pub fn from_ast(
ast: &ast::Attrs,
validator: &(impl AttributeValidator + ?Sized),
parent_attrs: &Attrs,
errors: &mut ErrorStore,
) -> Self {
let mut this = parent_attrs.clone();
// Backends must support this since it applies to the macro/C code.
// No special inheritance, was already appropriately inherited in AST
this.abi_rename = ast.abi_rename.clone();
let support = validator.attrs_supported();
let backend = validator.primary_name();
for attr in &ast.attrs {
let mut auto_found = false;
match validator.satisfies_cfg(&attr.cfg, Some(&mut auto_found)) {
Ok(satisfies) if !satisfies => continue,
Err(e) => {
errors.push(e);
continue;
}
Ok(_) => {}
};
let path = attr.meta.path();
if let Some(path) = path.get_ident() {
let path = path.to_string();
let warn_auto = |errors: &mut ErrorStore| {
if auto_found {
errors.push(LoweringError::Other(format!(
"Diplomat attribute {path:?} gated on 'auto' but is not one that works with 'auto'"
)));
}
};
// Check against the set of attributes that can have platform support
if support.check_string(&path) == Some(false) {
maybe_error_unsupported(auto_found, &path, backend, errors);
continue;
}
match SpecialMethod::from_path_and_meta(&path, &attr.meta) {
Ok(Some(kind)) => {
if let Some(ref existing) = this.special_method {
errors.push(LoweringError::Other(format!(
"Multiple special method markers found on the same method, found {path} and {existing:?}"
)));
} else {
this.special_method = Some(kind);
}
}
Err(error) => errors.push(error),
Ok(None) => match path.as_str() {
// No match found in the special methods, check the other keywords
"disable" => {
if let Meta::Path(_) = attr.meta {
if this.disable {
errors.push(LoweringError::Other(
"Duplicate `disable` attribute".into(),
));
} else {
this.disable = true;
}
} else {
errors.push(LoweringError::Other(
"`disable` must be a simple path".into(),
))
}
warn_auto(errors);
}
"rename" => {
match RenameAttr::from_meta(&attr.meta) {
Ok(rename) => {
// We use the override extend mode: a single ast::Attrs
// will have had these attributes inherited into the list by appending
// to the end; so a later attribute in the list is more pertinent.
this.rename.extend(&rename);
}
Err(e) => errors.push(LoweringError::Other(format!(
"`rename` attr failed to parse: {e:?}"
))),
}
warn_auto(errors);
}
"namespace" => match StandardAttribute::from_meta(&attr.meta) {
Ok(StandardAttribute::String(s)) if s.is_empty() => {
this.namespace = None
}
Ok(StandardAttribute::String(s)) => this.namespace = Some(s),
Ok(_) | Err(_) => {
errors.push(LoweringError::Other(
"`namespace` must have a single string parameter".to_string(),
));
}
},
"error" => {
this.custom_errors = true;
}
_ => {
errors.push(LoweringError::Other(format!(
"Unknown diplomat attribute {path}: expected one of: `disable, rename, namespace, constructor, stringifier, comparison, named_constructor, getter, setter, indexer, error`"
)));
}
},
}
}
}
for attr in &ast.demo_attrs {
let path = attr.meta.path();
if let Some(path_ident) = path.get_ident() {
if path_ident == "external" {
this.demo_attrs.external = true;
} else if path_ident == "default_constructor" {
this.demo_attrs.default_constructor = true;
} else if path_ident == "generate" {
this.demo_attrs.generate = true;
} else if path_ident == "input" {
let meta_list = attr
.meta
.require_list()
.expect("Could not get MetaList, expected #[diplomat::demo(input(...))]");
meta_list
.parse_nested_meta(|meta| {
if meta.path.is_ident("label") {
let value = meta.value()?;
let s: syn::LitStr = value.parse()?;
this.demo_attrs.input_cfg.label = s.value();
Ok(())
} else if meta.path.is_ident("default_value") {
let value = meta.value()?;
let str_val: String;
let ahead = value.lookahead1();
if ahead.peek(syn::LitFloat) {
let s: syn::LitFloat = value.parse()?;
str_val = s.base10_parse::<f64>()?.to_string();
} else if ahead.peek(syn::LitInt) {
let s: syn::LitInt = value.parse()?;
str_val = s.base10_parse::<i64>()?.to_string();
} else {
let s: syn::LitStr = value.parse()?;
str_val = s.value();
}
this.demo_attrs.input_cfg.default_value = str_val;
Ok(())
} else {
Err(meta.error(format!(
"Unsupported ident {:?}",
meta.path.get_ident()
)))
}
})
.expect("Could not read input(...)");
} else if path_ident == "custom_func" {
let v = &attr.meta.require_name_value().unwrap().value;
if let syn::Expr::Lit(s) = v {
if let syn::Lit::Str(string) = &s.lit {
this.demo_attrs.custom_func = Some(string.value());
} else {
errors.push(LoweringError::Other(format!(
"#[diplomat::demo(custom_func={s:?}) must be a literal string."
)));
}
} else {
errors.push(LoweringError::Other(format!(
"#[diplomat::demo(custom_func={v:?}) must be a literal string."
)));
}
} else {
errors.push(LoweringError::Other(format!(
"Unknown demo_attr: {path_ident:?}"
)));
}
} else {
errors.push(LoweringError::Other(format!("Unknown demo_attr: {path:?}")));
}
}
this
}
/// Validate that this attribute is allowed in this context
pub(crate) fn validate(
&self,
validator: &(impl AttributeValidator + ?Sized),
mut context: AttributeContext,
errors: &mut ErrorStore,
) {
// use an exhaustive destructure so new attributes are handled
let Attrs {
disable,
namespace,
rename,
abi_rename,
special_method,
custom_errors,
demo_attrs: _,
} = &self;
if *disable && matches!(context, AttributeContext::EnumVariant(..)) {
errors.push(LoweringError::Other(
"`disable` cannot be used on enum variants".into(),
))
}
if let Some(ref special) = special_method {
if let AttributeContext::Method(method, self_id, ref mut special_method_presence) =
context
{
let check_param_count = |name: &str, count: usize, errors: &mut ErrorStore| {
if method.params.len() != count {
errors.push(LoweringError::Other(format!(
"{name} must have exactly {count} parameter{}",
if count == 1 { "" } else { "s" }
)))
}
};
let check_self_param = |name: &str, need_self: bool, errors: &mut ErrorStore| {
if method.param_self.is_some() != need_self {
errors.push(LoweringError::Other(format!(
"{name} must{} accept a self parameter",
if need_self { "" } else { " not" }
)));
}
};
match special {
SpecialMethod::Constructor | SpecialMethod::NamedConstructor(..) => {
check_self_param("Constructors", false, errors);
let output = method.output.success_type();
match method.output {
ReturnType::Infallible(_) => (),
ReturnType::Fallible(..) => {
// Only report an error if constructors *are* supported but failable constructors *arent*
if validator.attrs_supported().constructors
&& !validator.attrs_supported().fallible_constructors
{
errors.push(LoweringError::Other(
"This backend doesn't support fallible constructors"
.to_string(),
))
}
}
ReturnType::Nullable(..) => {
errors.push(LoweringError::Other("Diplomat doesn't support turning nullable methods into constructors".to_string()));
}
}
if let SuccessType::OutType(t) = &output {
if t.id() != Some(self_id) {
errors.push(LoweringError::Other(
"Constructors must return Self!".to_string(),
));
}
} else {
errors.push(LoweringError::Other(
"Constructors must return Self!".to_string(),
));
}
}
SpecialMethod::Getter(_) => {
if !method.params.is_empty() {
errors
.push(LoweringError::Other("Getter cannot have parameters".into()));
}
if method.param_self.is_none()
&& !validator.attrs_supported().static_accessors
{
errors.push(LoweringError::Other(format!("No self parameter on Getter {} but static_acessors are not supported",method.name.as_str())));
}
// Currently does not forbid nullable getters, could if desired
}
SpecialMethod::Setter(_) => {
if !matches!(method.output.success_type(), SuccessType::Unit) {
errors.push(LoweringError::Other("Setters must return unit".into()));
}
if method.param_self.is_none()
&& !validator.attrs_supported().static_accessors
{
errors.push(LoweringError::Other(format!("No self parameter on Setter {} but static_acessors are not supported",method.name.as_str())));
}
check_param_count("Setter", 1, errors);
// Currently does not forbid fallible setters, could if desired
}
SpecialMethod::Stringifier => {
if !method.params.is_empty() {
errors
.push(LoweringError::Other("Getter cannot have parameters".into()));
}
if !matches!(method.output.success_type(), SuccessType::Write) {
errors.push(LoweringError::Other(
"Stringifier must return string".into(),
));
}
}
SpecialMethod::Comparison => {
check_param_count("Comparator", 1, errors);
if special_method_presence.comparator {
errors.push(LoweringError::Other(
"Cannot define two comparators on the same type".into(),
));
}
special_method_presence.comparator = true;
// In the long run we can actually support heterogeneous comparators. Not a priority right now.
const COMPARATOR_ERROR: &str =
"Comparator's parameter must be identical to self";
check_self_param("Comparators", true, errors);
if let Some(ref selfty) = method.param_self {
if let Some(param) = method.params.first() {
match (&selfty.ty, ¶m.ty) {
(SelfType::Opaque(p), Type::Opaque(p2)) => {
if p.tcx_id != p2.tcx_id {
errors.push(LoweringError::Other(
COMPARATOR_ERROR.into(),
));
}
if p.owner.mutability != Mutability::Immutable
|| p2.owner.mutability != Mutability::Immutable
{
errors.push(LoweringError::Other(
"comparators must accept immutable parameters"
.into(),
));
}
if p2.optional.0 {
errors.push(LoweringError::Other(
"comparators must accept non-optional parameters"
.into(),
));
}
}
(SelfType::Struct(p), Type::Struct(p2)) => {
if p.tcx_id != p2.tcx_id {
errors.push(LoweringError::Other(
COMPARATOR_ERROR.into(),
));
}
}
(SelfType::Enum(p), Type::Enum(p2)) => {
if p.tcx_id != p2.tcx_id {
errors.push(LoweringError::Other(
COMPARATOR_ERROR.into(),
));
}
}
_ => {
errors.push(LoweringError::Other(COMPARATOR_ERROR.into()));
}
}
}
}
}
SpecialMethod::Iterator => {
if special_method_presence.iterator.is_some() {
errors.push(LoweringError::Other(
"Cannot mark type as iterator twice".into(),
));
}
check_param_count("Iterator", 0, errors);
// In theory we could support struct and enum iterators. The benefit is slight:
// it generates probably inefficient code whilst being rather weird when it comes to the
// "structs and enums convert across the boundary" norm for backends.
//
// Essentially, the `&mut self` behavior won't work right.
//
// Furthermore, in some backends (like Dart) defining an iterator may requiring adding fields,
// which may not be possible for enums, and would still be an odd-one-out field for structs.g s
check_self_param("Iterator", true, errors);
if let Some(this) = &method.param_self {
if !matches!(this.ty, SelfType::Opaque(..)) {
errors.push(LoweringError::Other(
"Iterators only allowed on opaques".into(),
))
}
}
if let ReturnType::Nullable(ref o) = method.output {
if let SuccessType::Unit = o {
errors.push(LoweringError::Other(
"Iterator method must return something".into(),
));
}
special_method_presence.iterator = Some(o.clone());
} else if let ReturnType::Infallible(SuccessType::OutType(
crate::hir::OutType::Opaque(
ref o @ crate::hir::OpaquePath {
optional: crate::hir::Optional(true),
..
},
),
)) = method.output
{
let mut o = o.clone();
o.optional = crate::hir::Optional(false);
special_method_presence.iterator =
Some(SuccessType::OutType(crate::hir::OutType::Opaque(o)));
} else {
errors.push(LoweringError::Other(
"Iterator method must return nullable value".into(),
));
}
}
SpecialMethod::Iterable => {
if special_method_presence.iterable.is_some() {
errors.push(LoweringError::Other(
"Cannot mark type as iterable twice".into(),
));
}
check_param_count("Iterator", 0, errors);
check_self_param("Iterables", true, errors);
match method.output.success_type() {
SuccessType::OutType(ty) => {
if let Some(TypeId::Opaque(id)) = ty.id() {
special_method_presence.iterable = Some(id);
} else {
errors.push(LoweringError::Other(
"Iterables must return a custom opaque type".into(),
))
}
}
_ => errors.push(LoweringError::Other(
"Iterables must return a custom type".into(),
)),
}
}
SpecialMethod::Indexer => {
check_param_count("Indexer", 1, errors);
check_self_param("Indexer", true, errors);
if method.output.success_type().is_unit() {
errors.push(LoweringError::Other("Indexer must return a value".into()));
}
}
e @ (SpecialMethod::Add
| SpecialMethod::Sub
| SpecialMethod::Mul
| SpecialMethod::Div) => {
let name = match e {
SpecialMethod::Add => "Add",
SpecialMethod::Sub => "Sub",
SpecialMethod::Mul => "Mul",
SpecialMethod::Div => "Div",
_ => unreachable!(),
};
check_param_count(name, 1, errors);
check_self_param(name, true, errors);
if method.output.success_type().is_unit() {
errors
.push(LoweringError::Other(format!("{name} must return a value")));
}
}
e @ (SpecialMethod::AddAssign
| SpecialMethod::SubAssign
| SpecialMethod::MulAssign
| SpecialMethod::DivAssign) => {
let name = match e {
SpecialMethod::AddAssign => "AddAssign",
SpecialMethod::SubAssign => "SubAssign",
SpecialMethod::MulAssign => "MulAssign",
SpecialMethod::DivAssign => "DivAssign",
_ => unreachable!(),
};
check_param_count(name, 1, errors);
check_self_param(name, true, errors);
if let Some(self_param) = &method.param_self {
if matches!(self_param.ty, SelfType::Struct(_) | SelfType::Enum(_)) {
errors.push(LoweringError::Other("*Assign arithmetic operations not allowed on non-opaque types. \
Use the non-mutating arithmetic operators instead".to_string()));
} else if self_param.ty.is_immutably_borrowed() {
errors.push(LoweringError::Other(format!(
"{name} must take self by mutable reference"
)));
}
}
if !method.output.success_type().is_unit() {
errors.push(LoweringError::Other(format!(
"{name} must not return a value"
)));
}
}
}
} else {
errors.push(LoweringError::Other(format!("Special method (type {special:?}) not allowed on non-method context {context:?}")))
}
}
if namespace.is_some()
&& matches!(
context,
AttributeContext::Method(..) | AttributeContext::EnumVariant(..)
)
{
errors.push(LoweringError::Other(
"`namespace` can only be used on types".to_string(),
));
}
if matches!(
context,
AttributeContext::Param | AttributeContext::SelfParam | AttributeContext::Field
) {
if *disable {
errors.push(LoweringError::Other(format!(
"`disable`s cannot be used on an {context:?}."
)));
}
if namespace.is_some() {
errors.push(LoweringError::Other(format!(
"`namespace` cannot be used on an {context:?}."
)));
}
if !rename.is_empty() || !abi_rename.is_empty() {
errors.push(LoweringError::Other(format!(
"`rename`s cannot be used on an {context:?}."
)));
}
if special_method.is_some() {
errors.push(LoweringError::Other(format!(
"{context:?} cannot be special methods."
)));
}
}
if *custom_errors
&& !matches!(
context,
AttributeContext::Type(..) | AttributeContext::Trait(..)
)
{
errors.push(LoweringError::Other(
"`error` can only be used on types".to_string(),
));
}
}
pub(crate) fn for_inheritance(&self, context: AttrInheritContext) -> Attrs {
let rename = self.rename.attrs_for_inheritance(context, false);
// Disabling shouldn't inherit to variants
let disable = if context == AttrInheritContext::Variant {
false
} else {
self.disable
};
let namespace = if matches!(
context,
AttrInheritContext::Module | AttrInheritContext::Type
) {
self.namespace.clone()
} else {
None
};
Attrs {
disable,
rename,
namespace,
// Was already inherited on the AST side
abi_rename: Default::default(),
// Never inherited
special_method: None,
// Not inherited
custom_errors: false,
demo_attrs: Default::default(),
}
}
}
/// Non-exhaustive list of what attributes and other features your backend is able to handle, based on #[diplomat::attr(...)] contents.
/// Set this through an [`AttributeValidator`].
///
/// See [`SpecialMethod`] and [`Attrs`] for your specific implementation needs.
///
/// For example, the current dart backend supports [`BackendAttrSupport::constructors`]. So when it encounters:
/// ```ignore
/// struct Sample {}
/// impl Sample {
/// #[diplomat::attr(constructor)]
/// pub fn new() -> Box<Self> {
/// Box::new(Sample{})
/// }
/// }
///
/// ```
///
/// It generates
/// ```dart
/// factory Sample()
/// ```
///
/// If a backend does not support a specific `#[diplomat::attr(...)]`, it may error.
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Default)]
pub struct BackendAttrSupport {
/// Namespacing types, e.g. C++ `namespace`.
pub namespacing: bool,
/// Rust can directly acccess the memory of this language, like C and C++.
/// This is not supported in any garbage-collected language.
pub memory_sharing: bool,
/// This language's structs are non-exhaustive by default, i.e. adding
/// fields is not a breaking change.
pub non_exhaustive_structs: bool,
/// Whether the language supports method overloading
pub method_overloading: bool,
/// Whether the language uses UTF-8 strings
pub utf8_strings: bool,
/// Whether the language uses UTF-16 strings
pub utf16_strings: bool,
/// Whether the language supports using slices with 'static lifetimes.
pub static_slices: bool,
// Special methods
/// Marking a method as a constructor to generate special constructor methods.
pub constructors: bool,
/// Marking a method as a named constructor to generate special named constructor methods.
pub named_constructors: bool,
/// Marking constructors as being able to return errors. This is possible in languages where
/// errors are thrown as exceptions (Dart), but not for example in C++, where errors are
/// returned as values (constructors usually have to return the type itself).
pub fallible_constructors: bool,
/// Marking methods as field getters and setters, see [`SpecialMethod::Getter`] and [`SpecialMethod::Setter`]
pub accessors: bool,
/// Marking *static* methods as field getters and setters, see [`SpecialMethod::Getter`] and [`SpecialMethod::Setter`]
pub static_accessors: bool,
/// Marking a method as the `to_string` method, which is special in this language.
pub stringifiers: bool,
/// Marking a method as the `compare_to` method, which is special in this language.
pub comparators: bool,
/// Marking a method as the `next` method, which is special in this language.
pub iterators: bool,
/// Marking a method as the `iterator` method, which is special in this language.
pub iterables: bool,
/// Marking a method as the `[]` operator, which is special in this language.
pub indexing: bool,
/// Marking a method as an arithmetic operator (+-*/[=])
pub arithmetic: bool,
/// Support for Option<Struct> and Option<Primitive>
pub option: bool,
/// Allowing callback arguments
pub callbacks: bool,
/// Allowing traits
pub traits: bool,
/// Marking a user-defined type as being a valid error result type.
pub custom_errors: bool,
/// Traits are safe to Send between threads (safe to mark as std::marker::Send)
pub traits_are_send: bool,
/// Traits are safe to Sync between threads (safe to mark as std::marker::Sync)
pub traits_are_sync: bool,
}
impl BackendAttrSupport {
#[cfg(test)]
fn all_true() -> Self {
Self {
namespacing: true,
memory_sharing: true,
non_exhaustive_structs: true,
method_overloading: true,
utf8_strings: true,
utf16_strings: true,
static_slices: true,
constructors: true,
named_constructors: true,
fallible_constructors: true,
static_accessors: true,
accessors: true,
stringifiers: true,
comparators: true,
iterators: true,
iterables: true,
indexing: true,
arithmetic: true,
option: true,
callbacks: true,
traits: true,
custom_errors: true,
traits_are_send: true,
traits_are_sync: true,
}
}
fn check_string(&self, v: &str) -> Option<bool> {
match v {
"namespacing" => Some(self.namespacing),
"memory_sharing" => Some(self.memory_sharing),
"non_exhaustive_structs" => Some(self.non_exhaustive_structs),
"method_overloading" => Some(self.method_overloading),
"utf8_strings" => Some(self.utf8_strings),
"utf16_strings" => Some(self.utf16_strings),
"static_slices" => Some(self.static_slices),
"constructors" => Some(self.constructors),
"named_constructors" => Some(self.named_constructors),
"fallible_constructors" => Some(self.fallible_constructors),
"accessors" => Some(self.accessors),
"stringifiers" => Some(self.stringifiers),
"comparators" => Some(self.comparators),
"iterators" => Some(self.iterators),
"iterables" => Some(self.iterables),
"indexing" => Some(self.indexing),
"arithmetic" => Some(self.arithmetic),
"option" => Some(self.option),
"callbacks" => Some(self.callbacks),
"traits" => Some(self.traits),
"custom_errors" => Some(self.custom_errors),
"traits_are_send" => Some(self.traits_are_send),
"traits_are_sync" => Some(self.traits_are_sync),
_ => None,
}
}
}
/// Defined by backends when validating attributes
pub trait AttributeValidator {
/// The primary name of the backend, for use in diagnostics
fn primary_name(&self) -> &str;
/// Does this backend satisfy `cfg(backend_name)`?
/// (Backends are allowed to satisfy multiple backend names, useful when there
/// are multiple backends for a language)
fn is_backend(&self, backend_name: &str) -> bool;
/// does this backend satisfy cfg(name = value)?
fn is_name_value(&self, name: &str, value: &str) -> Result<bool, LoweringError>;
/// What backedn attrs does this support?
fn attrs_supported(&self) -> BackendAttrSupport;
/// Provided, checks if type satisfies a `DiplomatBackendAttrCfg`
///
/// auto_found helps check for `auto`, which is only allowed within `any` and at the top level. When `None`,
/// `auto` is not allowed.
fn satisfies_cfg(
&self,
cfg: &DiplomatBackendAttrCfg,
mut auto_found: Option<&mut bool>,
) -> Result<bool, LoweringError> {
Ok(match *cfg {
DiplomatBackendAttrCfg::Not(ref c) => !self.satisfies_cfg(c, None)?,
DiplomatBackendAttrCfg::Any(ref cs) => {
#[allow(clippy::needless_option_as_deref)]
// False positive: we need this for reborrowing
for c in cs {
if self.satisfies_cfg(c, auto_found.as_deref_mut())? {
return Ok(true);
}
}
false
}
DiplomatBackendAttrCfg::All(ref cs) => {
for c in cs {
if !self.satisfies_cfg(c, None)? {
return Ok(false);
}
}
true
}
DiplomatBackendAttrCfg::Auto => {
if let Some(found) = auto_found {
*found = true;
return Ok(true);
} else {
return Err(LoweringError::Other("auto in diplomat::attr() is only allowed at the top level and within `any`".into()));
}
}
DiplomatBackendAttrCfg::Star => true,
DiplomatBackendAttrCfg::BackendName(ref n) => self.is_backend(n),
DiplomatBackendAttrCfg::NameValue(ref n, ref v) => self.is_name_value(n, v)?,
})
}
// Provided, constructs an attribute
fn attr_from_ast(
&self,
ast: &ast::Attrs,
parent_attrs: &Attrs,
errors: &mut ErrorStore,
) -> Attrs {
Attrs::from_ast(ast, self, parent_attrs, errors)
}
// Provided: validates an attribute in the context in which it was constructed
fn validate(&self, attrs: &Attrs, context: AttributeContext, errors: &mut ErrorStore) {
attrs.validate(self, context, errors)
}
}
/// A basic attribute validator
#[non_exhaustive]
#[derive(Default)]
pub struct BasicAttributeValidator {
/// The primary name of this backend (should be unique, ideally)
pub backend_name: String,
/// The attributes supported
pub support: BackendAttrSupport,
/// Additional names for this backend
pub other_backend_names: Vec<String>,
/// override is_name_value()
#[allow(clippy::type_complexity)] // dyn fn is not that complex
pub is_name_value: Option<Box<dyn Fn(&str, &str) -> bool>>,
}
impl BasicAttributeValidator {
pub fn new(backend_name: &str) -> Self {
BasicAttributeValidator {
backend_name: backend_name.into(),
..Self::default()
}
}
}
impl AttributeValidator for BasicAttributeValidator {
fn primary_name(&self) -> &str {
&self.backend_name
}
fn is_backend(&self, backend_name: &str) -> bool {
self.backend_name == backend_name
|| self.other_backend_names.iter().any(|n| n == backend_name)
}
fn is_name_value(&self, name: &str, value: &str) -> Result<bool, LoweringError> {
Ok(if name == "supports" {
// destructure so new fields are forced to be added
let BackendAttrSupport {
namespacing,
memory_sharing,
non_exhaustive_structs,
method_overloading,
utf8_strings,
utf16_strings,
static_slices,
constructors,
named_constructors,
fallible_constructors,
accessors,
static_accessors,
stringifiers,
comparators,
iterators,
iterables,
indexing,
arithmetic,
option,
callbacks,
traits,
custom_errors,
traits_are_send,
traits_are_sync,
} = self.support;
match value {
"namespacing" => namespacing,
"memory_sharing" => memory_sharing,
"non_exhaustive_structs" => non_exhaustive_structs,
"method_overloading" => method_overloading,
"utf8_strings" => utf8_strings,
"utf16_strings" => utf16_strings,
"static_slices" => static_slices,
"constructors" => constructors,
"named_constructors" => named_constructors,
"fallible_constructors" => fallible_constructors,
"accessors" => accessors,
"static_accessors" => static_accessors,
"stringifiers" => stringifiers,
"comparators" => comparators,
"iterators" => iterators,
"iterables" => iterables,
"indexing" => indexing,
"arithmetic" => arithmetic,
"option" => option,
"callbacks" => callbacks,
"traits" => traits,
"custom_errors" => custom_errors,
"traits_are_send" => traits_are_send,
"traits_are_sync" => traits_are_sync,
_ => {
return Err(LoweringError::Other(format!(
"Unknown supports = value found: {value}"
)))
}
}
} else if let Some(ref nv) = self.is_name_value {
nv(name, value)
} else {
false
})
}
fn attrs_supported(&self) -> BackendAttrSupport {
self.support
}
}
#[cfg(test)]
mod tests {
use crate::hir;
use std::fmt::Write;
macro_rules! uitest_lowering_attr {
($attrs:expr, $($file:tt)*) => {
let parsed: syn::File = syn::parse_quote! { $($file)* };
let mut output = String::new();
let mut attr_validator = hir::BasicAttributeValidator::new("tests");
attr_validator.support = $attrs;
match hir::TypeContext::from_syn(&parsed, Default::default(), attr_validator) {
Ok(_context) => (),
Err(e) => {
for (ctx, err) in e {
writeln!(&mut output, "Lowering error in {ctx}: {err}").unwrap();
}
}
};
insta::with_settings!({}, {
insta::assert_snapshot!(output)
});
}
}
#[test]
fn test_auto() {
uitest_lowering_attr! { hir::BackendAttrSupport { comparators: true, ..Default::default()},
#[diplomat::bridge]
mod ffi {
use std::cmp;
#[diplomat::opaque]
#[diplomat::attr(auto, namespace = "should_not_show_up")]
struct Opaque;
impl Opaque {
#[diplomat::attr(auto, comparison)]
pub fn comparator_static(&self, other: &Opaque) -> cmp::Ordering {
todo!()
}
#[diplomat::attr(*, iterator)]
pub fn next(&mut self) -> Option<u8> {
self.0.next()
}
#[diplomat::attr(auto, rename = "bar")]
pub fn auto_doesnt_work_on_renames(&self) {
}
#[diplomat::attr(auto, disable)]
pub fn auto_doesnt_work_on_disables(&self) {
}
}
}
}
}
#[test]
fn test_comparator() {
uitest_lowering_attr! { hir::BackendAttrSupport::all_true(),
#[diplomat::bridge]
mod ffi {
use std::cmp;
#[diplomat::opaque]
struct Opaque;
struct Struct {
field: u8
}
impl Opaque {
#[diplomat::attr(auto, comparison)]
pub fn comparator_static(other: &Opaque) -> cmp::Ordering {
todo!()
}
#[diplomat::attr(auto, comparison)]
pub fn comparator_none(&self) -> cmp::Ordering {
todo!()
}
#[diplomat::attr(auto, comparison)]
pub fn comparator_othertype(other: Struct) -> cmp::Ordering {
todo!()
}
#[diplomat::attr(auto, comparison)]
pub fn comparator_badreturn(&self, other: &Opaque) -> u8 {
todo!()
}
#[diplomat::attr(auto, comparison)]
pub fn comparison_correct(&self, other: &Opaque) -> cmp::Ordering {
todo!()
}
pub fn comparison_unmarked(&self, other: &Opaque) -> cmp::Ordering {
todo!()
}
pub fn ordering_wrong(&self, other: cmp::Ordering) {
todo!()
}
#[diplomat::attr(auto, comparison)]
pub fn comparison_mut(&self, other: &mut Opaque) -> cmp::Ordering {
todo!()
}
#[diplomat::attr(auto, comparison)]
pub fn comparison_opt(&self, other: Option<&Opaque>) -> cmp::Ordering {
todo!()
}
}
impl Struct {
#[diplomat::attr(auto, comparison)]
pub fn comparison_other(self, other: &Opaque) -> cmp::Ordering {
todo!()
}
#[diplomat::attr(auto, comparison)]
pub fn comparison_correct(self, other: Self) -> cmp::Ordering {
todo!()
}
}
}
}
}
#[test]
fn test_iterator() {
uitest_lowering_attr! { hir::BackendAttrSupport::all_true(),
#[diplomat::bridge]
mod ffi {
#[diplomat::opaque]
struct Opaque(Vec<u8>);
#[diplomat::opaque]
struct OpaqueIterator<'a>(std::slice::Iter<'a>);
impl Opaque {
#[diplomat::attr(auto, iterable)]
pub fn iterable<'a>(&'a self) -> Box<OpaqueIterator<'a>> {
Box::new(OpaqueIterator(self.0.iter()))
}
}
impl OpaqueIterator {
#[diplomat::attr(auto, iterator)]
pub fn next(&mut self) -> Option<u8> {
self.0.next()
}
}
#[diplomat::opaque]
struct Broken;
impl Broken {
#[diplomat::attr(auto, iterable)]
pub fn iterable_no_return(&self) {}
#[diplomat::attr(auto, iterable)]
pub fn iterable_no_self() -> Box<BrokenIterator> { todo!() }
#[diplomat::attr(auto, iterable)]
pub fn iterable_non_custom(&self) -> u8 { todo!() }
}
#[diplomat::opaque]
struct BrokenIterator;
impl BrokenIterator {
#[diplomat::attr(auto, iterator)]
pub fn iterator_no_return(&self) {}
#[diplomat::attr(auto, iterator)]
pub fn iterator_no_self() -> Option<u8> { todo!() }
#[diplomat::attr(auto, iterator)]
pub fn iterator_no_option(&self) -> u8 { todo!() }
}
}
}
}
#[test]
fn test_unsupported_features() {
uitest_lowering_attr! { hir::BackendAttrSupport::default(),
#[diplomat::bridge]
mod ffi {
use std::cmp;
use diplomat_runtime::DiplomatOption;
#[diplomat::opaque]
struct Opaque;
struct Struct {
pub a: u8,
pub b: u8,
pub c: DiplomatOption<u8>,
}
struct Struct2 {
pub a: DiplomatOption<Struct>,
}
#[diplomat::out]
struct OutStruct {
pub option: DiplomatOption<u8>
}
impl Opaque {
pub fn take_option(&self, option: DiplomatOption<u8>) {
todo!()
}
// Always ok since this translates to a Resulty return
pub fn returning_option_is_ok(&self) -> Option<u8> {
todo!()
}
}
}
}
}
}