use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
use ridl_ir::name::{camel_case, snake_case};
use ridl_ir::projection::flatbuffers as fb_projection;
use ridl_ir::v2;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
mod clauses;
mod codec;
mod defaults;
mod derives;
mod descriptors;
mod face;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Generated {
pub rust_source: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerateError {
pub message: String,
}
pub fn generate(package: &v2::Package) -> Result<Generated, GenerateError> {
let ctx = Ctx::new(package);
render(package_items(&ctx, package)?)
}
pub fn generate_face(package: &v2::Package) -> Result<Generated, GenerateError> {
generate_face_with(package, WireEncoding::default())
}
pub fn generate_face_with(
package: &v2::Package,
wire: WireEncoding,
) -> Result<Generated, GenerateError> {
refuse_wire_collision(package)?;
let ctx = Ctx::new(package);
let mut items = vec![wire_alias(wire)];
items.extend(package_items(&ctx, package)?);
items.extend(descriptors::interface_items(&ctx, package)?);
items.extend(face::interface_items(package)?);
render(items)
}
pub fn generate_with(
package: &v2::Package,
others: &[&v2::Package],
) -> Result<Generated, GenerateError> {
let ctx = Ctx::with_others(package, others);
render(package_items(&ctx, package)?)
}
pub fn generate_pipeline(
package: &v2::Package,
wire: WireEncoding,
others: &[&v2::Package],
) -> Result<Generated, GenerateError> {
refuse_wire_collision(package)?;
let ctx = Ctx::with_others(package, others);
let mut items = vec![wire_alias(wire)];
items.extend(package_items(&ctx, package)?);
for shape in package.shapes() {
if shape.service.is_some() {
continue;
}
match faced_interface(&ctx, package, shape.name, shape.interface) {
Ok(produced) => items.extend(produced),
Err(err) => items.push(skipped_interface_note(shape.name, shape.interface, &err)),
}
}
render(items)
}
fn faced_interface(
ctx: &Ctx,
package: &v2::Package,
iface_name: &str,
interface: &v2::Interface,
) -> Result<Vec<TokenStream>, GenerateError> {
let mut items = descriptors::one_interface_items(ctx, &package.name, iface_name, interface)?;
if let Some(module) = face::one_interface(iface_name, interface)? {
items.push(module);
}
Ok(items)
}
fn skipped_interface_note(
iface_name: &str,
interface: &v2::Interface,
err: &GenerateError,
) -> TokenStream {
let name = format_ident!("__RIDL_NO_FACE_{}", snake_case(iface_name).to_uppercase());
let headline = format!(" Interface `{iface_name}` carries no generated interaction face.");
let reason = format!(" The emitter refused it: {}", err.message);
let owner = match face_gap(interface, err) {
FaceGap::CallShape => {
" A call the face cannot carry — an interaction that does not declare \
exactly one named parameter, or a query whose reply is not a named \
type. The induced argument struct that removes the first is lane M's \
parked multi-parameter follow-up."
}
FaceGap::Clause => {
" A contract clause outside the form the narrow translator accepts — \
`<subject> <comparison> <numeric literal>`, conjoined with `&&`. \
Story E5.1 replaces the translator and removes this."
}
FaceGap::Other => " No story below owns this one: the reason above is the whole of it.",
};
quote! {
#[doc = #headline]
#[doc = #reason]
#[doc = #owner]
#[allow(dead_code)]
const #name: () = ();
}
}
enum FaceGap {
CallShape,
Clause,
Other,
}
fn face_gap(interface: &v2::Interface, err: &GenerateError) -> FaceGap {
if err.message.starts_with(clauses::CLAUSE_REFUSAL) {
return FaceGap::Clause;
}
for decl in &interface.interactions {
let params = match decl.kind.as_ref() {
Some(v2::decl::Kind::CommandDef(command)) => &command.params,
Some(v2::decl::Kind::QueryDef(query)) => {
if descriptors::query_reply_type(query, &decl.name).is_err() {
return FaceGap::CallShape;
}
&query.params
}
_ => continue,
};
if descriptors::single_param_type(params, &decl.name).is_err() {
return FaceGap::CallShape;
}
}
FaceGap::Other
}
const WIRE_ALIAS: &str = "Wire";
fn refuse_wire_collision(package: &v2::Package) -> Result<(), GenerateError> {
let declared = package
.decls
.iter()
.map(|decl| (decl.name.as_str(), "declaration"));
let shapes = package
.shapes()
.filter(|shape| shape.service.is_none())
.map(|shape| (shape.name, "interface"));
for (name, kind) in declared.chain(shapes) {
if name == WIRE_ALIAS {
return Err(GenerateError {
message: format!(
"`{}.{}` collides with the `{}` encoding alias the interaction \
face emits at package scope; rename the {kind}",
package.name, name, WIRE_ALIAS
),
});
}
}
Ok(())
}
fn package_items(ctx: &Ctx, package: &v2::Package) -> Result<Vec<TokenStream>, GenerateError> {
let (mut items, tuples) = domain_items(ctx, package)?;
items.extend(codec::package_items(ctx, package, &tuples)?);
Ok(items)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum WireEncoding {
#[default]
FlatBuffers,
}
fn wire_alias(wire: WireEncoding) -> TokenStream {
let (path, doc) = match wire {
WireEncoding::FlatBuffers => (
quote! { ::ridl_rt::encoding::FlatBuffers },
"The payload encoding this package's generated interaction face \
encodes and verifies over, and the one the `Payload` \
implementations below implement: FlatBuffers (ADR-0019, ADR-0020 \
decision 2). It is named once here rather than repeated at every \
buffer and every `Ref` the face builds, so the package's \
encoding is one line to read and one line to change.",
),
};
quote! {
#[doc = #doc]
pub type Wire = #path;
}
}
#[allow(clippy::type_complexity)]
fn domain_items(
ctx: &Ctx,
package: &v2::Package,
) -> Result<(Vec<TokenStream>, Vec<InducedTuple>), GenerateError> {
let mut items: Vec<TokenStream> = Vec::new();
let mut tuples: Vec<InducedTuple> = Vec::new();
let mut discovered: Vec<InducedTuple> = Vec::new();
for decl in &package.decls {
items.push(emit_decl(ctx, decl, &mut tuples));
}
let mut seen: HashMap<String, InducedTuple> = HashMap::new();
let mut index = 0;
while index < tuples.len() {
let induced = tuples[index].clone();
index += 1;
if let Some(previous) = seen.get(&induced.name) {
if previous.tuple != induced.tuple || previous.visibility != induced.visibility {
return Err(tuple_collision(previous, &induced));
}
continue;
}
seen.insert(induced.name.clone(), induced.clone());
items.push(emit_tuple_struct(ctx, &induced, &mut tuples));
discovered.push(induced);
}
Ok((items, discovered))
}
pub(crate) fn check_flatbuffers_bound(
ctx: &Ctx,
package: &v2::Package,
decl: &v2::Decl,
) -> Result<(), GenerateError> {
if fb_projection::root_table(decl).is_none() {
return Ok(());
}
let packages = fb_projection::Packages {
package,
others: &[],
};
if fb_projection::max_size(packages, decl).is_some() {
return Ok(());
}
let pkg = &package.name;
let name = &decl.name;
match unbounded_member(ctx, package, decl) {
Attribution::Member(member) => Err(GenerateError {
message: format!("`{pkg}.{name}.{member}` has no finite FlatBuffers bound"),
}),
Attribution::Untyped(member) => Err(GenerateError {
message: format!(
"`{pkg}.{name}.{member}` carries no type, so `{pkg}.{name}` has no FlatBuffers \
bound"
),
}),
Attribution::Layout(message) => Err(GenerateError {
message: format!("`{pkg}.{name}` has no FlatBuffers table layout: {message}"),
}),
Attribution::Aggregate => Err(GenerateError {
message: format!(
"`{pkg}.{name}` has no finite FlatBuffers bound: every member is bounded on its \
own and the total is not"
),
}),
Attribution::Exempt => Ok(()),
}
}
enum Attribution {
Member(String),
Untyped(String),
Layout(String),
Aggregate,
Exempt,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Verdict {
Bounded,
Unbounded,
Unjudgeable,
}
fn unbounded_member(ctx: &Ctx, package: &v2::Package, decl: &v2::Decl) -> Attribution {
let mut any_exempt = false;
match &decl.kind {
Some(v2::decl::Kind::StructDef(def)) => {
if let Err(err) = fb_projection::struct_table(&decl.name, def) {
return Attribution::Layout(err.message);
}
for member in &def.members {
let Some(v2::struct_member::Member::Field(field)) = &member.member else {
continue;
};
let Some(ty) = field.r#type.as_ref() else {
return Attribution::Untyped(field.name.clone());
};
match judge(ctx, package, ty) {
Verdict::Unbounded => return Attribution::Member(field.name.clone()),
Verdict::Unjudgeable => any_exempt = true,
Verdict::Bounded => {}
}
}
}
Some(v2::decl::Kind::UnionDef(def)) => {
for arm in &def.arms {
let ty = v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(arm.type_ref.clone())),
};
match judge(ctx, package, &ty) {
Verdict::Unbounded => return Attribution::Member(arm.name.clone()),
Verdict::Unjudgeable => any_exempt = true,
Verdict::Bounded => {}
}
}
}
Some(
v2::decl::Kind::TypeDef(_) | v2::decl::Kind::EnumDef(_) | v2::decl::Kind::EnumSetDef(_),
) => {
if let Some(v2::decl::Kind::TypeDef(td)) = &decl.kind
&& td.backing.is_none()
{
return Attribution::Untyped("value".to_string());
}
let ty = v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(decl.name.clone())),
};
match judge(ctx, package, &ty) {
Verdict::Unbounded => return Attribution::Member("value".to_string()),
Verdict::Unjudgeable => any_exempt = true,
Verdict::Bounded => {}
}
}
_ => {}
}
if !any_exempt {
return Attribution::Aggregate;
}
if let Some(v2::decl::Kind::StructDef(def)) = &decl.kind {
let stand_in = v2::Decl {
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: def
.members
.iter()
.map(|member| match &member.member {
Some(v2::struct_member::Member::Field(field)) => v2::StructMember {
member: Some(v2::struct_member::Member::Field(v2::Field {
r#type: field
.r#type
.as_ref()
.map(|ty| lower_bound_stand_in(ctx, ty)),
..field.clone()
})),
},
_ => member.clone(),
})
.collect(),
fixed_layout: def.fixed_layout,
})),
..decl.clone()
};
let packages = fb_projection::Packages {
package,
others: &[],
};
if fb_projection::max_size(packages, &stand_in).is_none() {
return Attribution::Aggregate;
}
}
Attribution::Exempt
}
fn judge(ctx: &Ctx, package: &v2::Package, ty: &v2::FieldType) -> Verdict {
if member_resolves_locally(ctx, ty) {
return if probe_field_type(package, ty).is_some() {
Verdict::Bounded
} else {
Verdict::Unbounded
};
}
if probe_field_type(package, &lower_bound_stand_in(ctx, ty)).is_some() {
Verdict::Unjudgeable
} else {
Verdict::Unbounded
}
}
fn lower_bound_stand_in(ctx: &Ctx, ty: &v2::FieldType) -> v2::FieldType {
fn boolean(optional: bool) -> v2::FieldType {
v2::FieldType {
optional,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
}
}
let stand_in = |leaf: &v2::FieldType| Box::new(lower_bound_stand_in(ctx, leaf));
let kind = match ty.kind.as_ref() {
Some(v2::field_type::Kind::Array(array)) => {
v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: array.element.as_deref().map(stand_in),
min: array.min,
max: array.max,
}))
}
Some(v2::field_type::Kind::Map(map)) => v2::field_type::Kind::Map(Box::new(v2::MapType {
key: map.key.as_deref().map(stand_in),
value: map.value.as_deref().map(stand_in),
min: map.min,
max: map.max,
})),
Some(v2::field_type::Kind::Tuple(tuple)) => v2::field_type::Kind::Tuple(v2::TupleType {
fields: tuple
.fields
.iter()
.map(|field| v2::TupleField {
r#type: field.r#type.as_ref().map(|leaf| *stand_in(leaf)),
..field.clone()
})
.collect(),
}),
_ if member_resolves_locally(ctx, ty) => return ty.clone(),
_ => return boolean(ty.optional),
};
v2::FieldType {
optional: ty.optional,
kind: Some(kind),
}
}
pub(crate) fn unjudgeable_members(ctx: &Ctx, decl: &v2::Decl) -> Vec<String> {
let mut names = Vec::new();
match &decl.kind {
Some(v2::decl::Kind::StructDef(def)) => {
for member in &def.members {
if let Some(v2::struct_member::Member::Field(field)) = &member.member
&& let Some(ty) = field.r#type.as_ref()
&& !member_resolves_locally(ctx, ty)
{
names.push(field.name.clone());
}
}
}
Some(v2::decl::Kind::UnionDef(def)) => {
for arm in &def.arms {
if !decl_resolves_locally(ctx, &arm.type_ref, &mut HashSet::new()) {
names.push(arm.name.clone());
}
}
}
_ => {}
}
names
}
fn probe_field_type(package: &v2::Package, ty: &v2::FieldType) -> Option<u64> {
let probe = v2::Decl {
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![v2::StructMember {
member: Some(v2::struct_member::Member::Field(v2::Field {
ordinal: 1,
r#type: Some(ty.clone()),
..Default::default()
})),
}],
fixed_layout: false,
})),
..Default::default()
};
let packages = fb_projection::Packages {
package,
others: &[],
};
fb_projection::max_size(packages, &probe)
}
fn member_resolves_locally(ctx: &Ctx, ty: &v2::FieldType) -> bool {
field_type_resolves_locally(ctx, ty, &mut HashSet::new())
}
fn decl_resolves_locally(ctx: &Ctx, reference: &str, visiting: &mut HashSet<String>) -> bool {
let Some(decl) = ctx.lookup(reference) else {
return false;
};
if !visiting.insert(reference.to_string()) {
return false;
}
let resolves = match &decl.kind {
Some(v2::decl::Kind::StructDef(def)) => {
def.members.iter().all(|member| match &member.member {
Some(v2::struct_member::Member::Field(field)) => field
.r#type
.as_ref()
.is_some_and(|ty| field_type_resolves_locally(ctx, ty, visiting)),
_ => true,
})
}
Some(v2::decl::Kind::UnionDef(def)) => def
.arms
.iter()
.all(|arm| decl_resolves_locally(ctx, &arm.type_ref, visiting)),
_ => true,
};
visiting.remove(reference);
resolves
}
fn field_type_resolves_locally(
ctx: &Ctx,
ty: &v2::FieldType,
visiting: &mut HashSet<String>,
) -> bool {
match &ty.kind {
Some(v2::field_type::Kind::Named(reference)) => {
decl_resolves_locally(ctx, reference, visiting)
}
Some(v2::field_type::Kind::Primitive(primitive)) => v2::PrimitiveType::try_from(*primitive)
.is_ok_and(|primitive| primitive != v2::PrimitiveType::Unspecified),
Some(v2::field_type::Kind::InlineScalar(_)) => true,
Some(v2::field_type::Kind::Tuple(tuple)) => tuple.fields.iter().all(|field| {
field
.r#type
.as_ref()
.is_some_and(|ty| field_type_resolves_locally(ctx, ty, visiting))
}),
Some(v2::field_type::Kind::Array(array)) => array
.element
.as_deref()
.is_some_and(|element| field_type_resolves_locally(ctx, element, visiting)),
Some(v2::field_type::Kind::Map(map)) => {
map.key
.as_deref()
.is_some_and(|key| field_type_resolves_locally(ctx, key, visiting))
&& map
.value
.as_deref()
.is_some_and(|value| field_type_resolves_locally(ctx, value, visiting))
}
Some(v2::field_type::Kind::Stream(_)) => false,
None => true,
}
}
fn render(items: Vec<TokenStream>) -> Result<Generated, GenerateError> {
let tokens = quote! { #(#items)* };
let file: syn::File = syn::parse2(tokens).map_err(|err| GenerateError {
message: format!("generated Rust does not parse: {err}"),
})?;
Ok(Generated {
rust_source: prettyplease::unparse(&file),
})
}
#[derive(Debug, Clone)]
pub(crate) struct InducedTuple {
pub(crate) name: String,
pub(crate) tuple: v2::TupleType,
pub(crate) visibility: i32,
}
fn tuple_collision(previous: &InducedTuple, current: &InducedTuple) -> GenerateError {
fn shape(induced: &InducedTuple) -> String {
let fields: Vec<String> = induced
.tuple
.fields
.iter()
.map(|field| field.name.clone())
.collect();
format!("({})", fields.join(", "))
}
GenerateError {
message: format!(
"the generated name {name} is claimed by two different tuple types, {a} and {b}; \
a tuple generates a struct named for the path that reaches it, and these two paths \
spell one name — rename a field or a declaration so they differ",
name = current.name,
a = shape(previous),
b = shape(current),
),
}
}
pub(crate) struct Ctx<'a> {
decls: HashMap<&'a str, &'a v2::Decl>,
pub(crate) others: &'a [&'a v2::Package],
visiting: RefCell<HashSet<String>>,
}
impl<'a> Ctx<'a> {
pub(crate) fn new(package: &'a v2::Package) -> Self {
Ctx::with_others(package, &[])
}
pub(crate) fn with_others(package: &'a v2::Package, others: &'a [&'a v2::Package]) -> Self {
let decls = package
.decls
.iter()
.map(|decl| (decl.name.as_str(), decl))
.collect();
Ctx {
decls,
others,
visiting: RefCell::new(HashSet::new()),
}
}
pub(crate) fn lookup(&self, name: &str) -> Option<&'a v2::Decl> {
self.decls.get(name).copied()
}
pub(crate) fn enter_default(&self, name: &str) -> bool {
self.visiting.borrow_mut().insert(name.to_string())
}
pub(crate) fn leave_default(&self, name: &str) {
self.visiting.borrow_mut().remove(name);
}
}
fn emit_decl(ctx: &Ctx, decl: &v2::Decl, tuples: &mut Vec<InducedTuple>) -> TokenStream {
let derived = derives::derive_attr(ctx, decl);
let item = match &decl.kind {
Some(v2::decl::Kind::TypeDef(td)) => emit_type_def(decl, td, &derived),
Some(v2::decl::Kind::ConstDef(cd)) => return emit_const(ctx, decl, cd),
Some(v2::decl::Kind::StructDef(sd)) => emit_struct(decl, sd, &derived, tuples),
Some(v2::decl::Kind::EnumDef(ed)) => emit_enum(decl, ed, &derived),
Some(v2::decl::Kind::EnumSetDef(esd)) => emit_enum_set(decl, esd, &derived),
Some(v2::decl::Kind::UnionDef(ud)) => emit_union(decl, ud, &derived),
Some(_) | None => return quote! {},
};
let default_impl = defaults::decl_default_expr(ctx, decl)
.map(|expr| {
let name = ident(&decl.name);
quote! { impl Default for #name { fn default() -> Self { #expr } } }
})
.unwrap_or_default();
quote! { #item #default_impl }
}
fn emit_type_def(decl: &v2::Decl, td: &v2::TypeDef, derived: &TokenStream) -> TokenStream {
let name = ident(&decl.name);
let inner = newtype_inner(td);
let doc = doc_attrs(&decl.doc);
let unchecked = unchecked_doc(td);
let separator = if decl.doc.is_empty() || unchecked.is_empty() {
quote! {}
} else {
quote! { #[doc = ""] }
};
let deprecated = deprecated_attr(decl.deprecated.as_deref());
let allow_deprecated = if decl.deprecated.is_some() {
quote! { #[allow(deprecated)] }
} else {
quote! {}
};
let vis = vis_tokens(decl.visibility);
let type_name = decl.name.as_str();
if v2::constraint_is_vacuous(td.constraint.as_ref()) {
return emit_vacuous_type_def(decl, td, derived);
}
let check_param_ty = check_param_type(td);
let check_shadow = check_deref_shadow(td);
let check_body = constraint_checks(td, type_name, quote! { value });
let getter = scalar_getter(td, vis.clone(), inner.clone());
quote! {
#doc
#separator
#unchecked
#derived
#deprecated
#[repr(transparent)]
#vis struct #name(#inner);
#allow_deprecated
impl #name {
#vis fn new(
value: #inner,
) -> ::core::result::Result<Self, ::ridl_rt::payload::Violation> {
Self::check(&value)?;
::core::result::Result::Ok(Self::new_unchecked(value))
}
pub(crate) fn check(
value: #check_param_ty,
) -> ::core::result::Result<(), ::ridl_rt::payload::Violation> {
#check_shadow
#check_body
::core::result::Result::Ok(())
}
#vis const fn new_unchecked(value: #inner) -> Self {
Self(value)
}
#getter
}
#allow_deprecated
impl ::core::convert::TryFrom<#inner> for #name {
type Error = ::ridl_rt::payload::Violation;
fn try_from(value: #inner) -> ::core::result::Result<Self, Self::Error> {
Self::new(value)
}
}
#allow_deprecated
impl ::core::convert::From<#name> for #inner {
fn from(value: #name) -> Self {
value.0
}
}
}
}
fn emit_vacuous_type_def(decl: &v2::Decl, td: &v2::TypeDef, derived: &TokenStream) -> TokenStream {
let name = ident(&decl.name);
let inner = newtype_inner(td);
let doc = doc_attrs(&decl.doc);
let unchecked = unchecked_doc(td);
let separator = if decl.doc.is_empty() || unchecked.is_empty() {
quote! {}
} else {
quote! { #[doc = ""] }
};
let deprecated = deprecated_attr(decl.deprecated.as_deref());
let allow_deprecated = if decl.deprecated.is_some() {
quote! { #[allow(deprecated)] }
} else {
quote! {}
};
let vis = vis_tokens(decl.visibility);
let getter = scalar_getter(td, vis.clone(), inner.clone());
quote! {
#doc
#separator
#unchecked
#derived
#deprecated
#[repr(transparent)]
#vis struct #name(#inner);
#allow_deprecated
impl #name {
#vis const fn new(value: #inner) -> Self {
Self(value)
}
#getter
}
#allow_deprecated
impl ::core::convert::From<#inner> for #name {
fn from(value: #inner) -> Self {
Self(value)
}
}
#allow_deprecated
impl ::core::convert::From<#name> for #inner {
fn from(value: #name) -> Self {
value.0
}
}
}
}
pub(crate) fn scalar_ctor(td: &v2::TypeDef) -> TokenStream {
if v2::constraint_is_vacuous(td.constraint.as_ref()) {
quote! { new }
} else {
quote! { new_unchecked }
}
}
fn constraint_checks(td: &v2::TypeDef, type_name: &str, value: TokenStream) -> TokenStream {
let Some(c) = td.constraint.as_ref() else {
return quote! {};
};
let mut checks = Vec::new();
let is_float = match backing_scalar(td) {
ScalarBacking::Float => Some(true),
ScalarBacking::Integer => Some(false),
ScalarBacking::Boolean | ScalarBacking::String | ScalarBacking::Bytes => None,
};
if let Some(is_float) = is_float {
if let Some(min) = c.min.as_deref() {
let lit = numeric_tokens(min, is_float);
checks.push(quote! {
if #value < #lit {
return ::core::result::Result::Err(::ridl_rt::payload::Violation {
type_name: #type_name,
rule: ::ridl_rt::payload::Rule::Range,
});
}
});
}
let checked_max = c
.max
.as_deref()
.filter(|max| is_float || max.parse::<i64>() != Ok(i64::MAX));
if let Some(max) = checked_max {
let lit = numeric_tokens(max, is_float);
checks.push(quote! {
if #value > #lit {
return ::core::result::Result::Err(::ridl_rt::payload::Violation {
type_name: #type_name,
rule: ::ridl_rt::payload::Rule::Range,
});
}
});
}
}
if c.len_min.is_some() || c.len_max.is_some() {
let len = match backing_scalar(td) {
ScalarBacking::String => quote! { (#value.chars().count() as u64) },
_ => quote! { (#value.len() as u64) },
};
if let Some(min) = c.len_min.filter(|min| *min > 0) {
let lit = proc_macro2::Literal::u64_unsuffixed(min);
checks.push(quote! {
if #len < #lit {
return ::core::result::Result::Err(::ridl_rt::payload::Violation {
type_name: #type_name,
rule: ::ridl_rt::payload::Rule::Length,
});
}
});
}
if let Some(max) = c.len_max {
let lit = proc_macro2::Literal::u64_unsuffixed(max);
checks.push(quote! {
if #len > #lit {
return ::core::result::Result::Err(::ridl_rt::payload::Violation {
type_name: #type_name,
rule: ::ridl_rt::payload::Rule::Length,
});
}
});
}
}
if backing_scalar(td) == ScalarBacking::String
&& let Some(pattern) = c.pattern.as_deref()
{
let source = strip_regex_delimiters(pattern);
checks.push(quote! {
#[cfg(feature = "validate-pattern")]
{
static PATTERN: ::std::sync::LazyLock<::regex::Regex> =
::std::sync::LazyLock::new(|| {
::regex::Regex::new(#source).expect("ridlc emitted an invalid pattern")
});
if !PATTERN.is_match(&#value) {
return ::core::result::Result::Err(::ridl_rt::payload::Violation {
type_name: #type_name,
rule: ::ridl_rt::payload::Rule::Pattern,
});
}
}
});
}
quote! { #(#checks)* }
}
fn check_param_type(td: &v2::TypeDef) -> TokenStream {
match backing_scalar(td) {
ScalarBacking::Float => quote! { &f64 },
ScalarBacking::Integer => quote! { &i64 },
ScalarBacking::Boolean => quote! { &bool },
ScalarBacking::String => quote! { &str },
ScalarBacking::Bytes => quote! { &[u8] },
}
}
fn check_deref_shadow(td: &v2::TypeDef) -> TokenStream {
match backing_scalar(td) {
ScalarBacking::Float | ScalarBacking::Integer | ScalarBacking::Boolean => {
quote! { let value = *value; }
}
ScalarBacking::String | ScalarBacking::Bytes => quote! {},
}
}
fn scalar_getter(td: &v2::TypeDef, vis: TokenStream, inner: TokenStream) -> TokenStream {
match backing_scalar(td) {
ScalarBacking::String => quote! {
#vis fn get(&self) -> &str { &self.0 }
#vis fn into_inner(self) -> String { self.0 }
},
ScalarBacking::Bytes => quote! {
#vis fn get(&self) -> &[u8] { &self.0 }
#vis fn into_inner(self) -> Vec<u8> { self.0 }
},
_ => quote! {
#vis const fn get(self) -> #inner { self.0 }
},
}
}
fn unchecked_doc(td: &v2::TypeDef) -> TokenStream {
let Some(c) = td.constraint.as_ref() else {
return quote! {};
};
let mut lines = Vec::new();
if c.step.is_some() {
lines.push(" Quantization (`step`) is not checked by `new`.".to_string());
}
if c.pattern.is_some() && backing_scalar(td) == ScalarBacking::String {
lines.push(
" The `match` pattern is checked by `new` only when the crate is built with \
the `validate-pattern` feature."
.to_string(),
);
} else if c.pattern.is_some() || c.pattern_const.is_some() {
lines.push(" The `match` pattern is not checked by `new`.".to_string());
}
quote! { #(#[doc = #lines])* }
}
fn emit_const(ctx: &Ctx, decl: &v2::Decl, cd: &v2::ConstDef) -> TokenStream {
let attrs = decl_attrs(decl, "e! {});
let vis = vis_tokens(decl.visibility);
let name = ident(&decl.name);
if let Some(regex) = &cd.regex {
let pattern = strip_regex_delimiters(regex);
return quote! { #attrs #vis const #name: &str = #pattern; };
}
let Some(type_ref) = cd.type_ref.as_deref() else {
return quote! {};
};
if let Some(backing) = same_package_scalar_backing(ctx, type_ref) {
let ctor = same_package_scalar_ctor(ctx, type_ref)
.expect("a same-package scalar backing implies a same-package scalar ctor");
match backing {
ScalarBacking::Float => {
let value = numeric_tokens(&cd.value, true);
let type_name = type_path(type_ref);
quote! { #attrs #vis const #name: #type_name = #type_name::#ctor(#value); }
}
ScalarBacking::Integer => {
let value = numeric_tokens(&cd.value, false);
let type_name = type_path(type_ref);
quote! { #attrs #vis const #name: #type_name = #type_name::#ctor(#value); }
}
ScalarBacking::Boolean => {
let value = bool_tokens(&cd.value);
let type_name = type_path(type_ref);
quote! { #attrs #vis const #name: #type_name = #type_name::#ctor(#value); }
}
ScalarBacking::String => {
let value = cd.value.as_str();
quote! { #attrs #vis const #name: &str = #value; }
}
ScalarBacking::Bytes => quote! {},
}
} else if let Some(prim) = primitive_keyword(type_ref) {
match prim {
v2::PrimitiveType::Integer => {
let value = numeric_tokens(&cd.value, false);
quote! { #attrs #vis const #name: i64 = #value; }
}
v2::PrimitiveType::Float => {
let value = numeric_tokens(&cd.value, true);
quote! { #attrs #vis const #name: f64 = #value; }
}
v2::PrimitiveType::Boolean => {
let value = bool_tokens(&cd.value);
quote! { #attrs #vis const #name: bool = #value; }
}
v2::PrimitiveType::String => {
let value = cd.value.as_str();
quote! { #attrs #vis const #name: &str = #value; }
}
v2::PrimitiveType::Bytes | v2::PrimitiveType::Unspecified => quote! {},
}
} else {
quote! {}
}
}
fn emit_struct(
decl: &v2::Decl,
sd: &v2::StructDef,
derived: &TokenStream,
tuples: &mut Vec<InducedTuple>,
) -> TokenStream {
let name = ident(&decl.name);
let attrs = decl_attrs(decl, derived);
let vis = vis_tokens(decl.visibility);
let repr = if sd.fixed_layout {
quote! { #[repr(C)] }
} else {
quote! {}
};
let fields = sd.members.iter().filter_map(|member| match &member.member {
Some(v2::struct_member::Member::Field(field)) => {
Some(emit_field(&decl.name, decl.visibility, field, tuples))
}
Some(v2::struct_member::Member::Reserved(_)) | None => None,
});
quote! {
#attrs
#repr
#vis struct #name {
#(#fields),*
}
}
}
fn emit_field(
parent: &str,
visibility: i32,
field: &v2::Field,
tuples: &mut Vec<InducedTuple>,
) -> TokenStream {
let field_name = ident(&snake_case(&field.name));
let attrs = field_attrs(field);
let hint = format!("{}{}", camel_case(parent), camel_case(&field.name));
let ty = field
.r#type
.as_ref()
.map(|ft| field_type_tokens(ft, &hint, visibility, tuples))
.unwrap_or_else(|| quote! { () });
quote! { #attrs pub #field_name: #ty }
}
fn emit_enum(decl: &v2::Decl, ed: &v2::EnumDef, derived: &TokenStream) -> TokenStream {
let name = ident(&decl.name);
let attrs = decl_attrs(decl, derived);
let vis = vis_tokens(decl.visibility);
let variants = ed.values.iter().map(|value| {
let vname = ident(&value.name);
let disc = int_tokens(value.value);
let vdoc = doc_attrs(&value.doc);
quote! { #vdoc #vname = #disc }
});
let arms = ed.values.iter().map(|value| {
let vname = ident(&value.name);
let disc = int_tokens(value.value);
quote! { #disc => ::core::result::Result::Ok(Self::#vname) }
});
let type_name = decl.name.as_str();
let allow_deprecated = if decl.deprecated.is_some() {
quote! { #[allow(deprecated)] }
} else {
quote! {}
};
quote! {
#attrs
#[repr(i64)]
#vis enum #name {
#(#variants),*
}
#allow_deprecated
impl ::core::convert::TryFrom<i64> for #name {
type Error = ::ridl_rt::payload::Violation;
fn try_from(value: i64) -> ::core::result::Result<Self, Self::Error> {
match value {
#(#arms,)*
_ => ::core::result::Result::Err(::ridl_rt::payload::Violation {
type_name: #type_name,
rule: ::ridl_rt::payload::Rule::Variant,
}),
}
}
}
#allow_deprecated
impl ::core::convert::From<#name> for i64 {
fn from(value: #name) -> Self { value as i64 }
}
}
}
fn emit_enum_set(decl: &v2::Decl, esd: &v2::EnumSetDef, derived: &TokenStream) -> TokenStream {
let name = ident(&decl.name);
let attrs = decl_attrs(decl, derived);
let vis = vis_tokens(decl.visibility);
let bits = esd.bits.iter().map(|bit| {
let bname = ident(&bit.name);
let shift = int_tokens(bit.value);
quote! { #vis const #bname: #name = #name(1 << #shift); }
});
let mask = esd
.bits
.iter()
.filter(|bit| (0..=63).contains(&bit.value))
.fold(0i64, |acc, bit| acc | (1i64 << bit.value));
let mask_lit = int_tokens(mask);
let type_name = decl.name.as_str();
let allow_deprecated = if decl.deprecated.is_some() {
quote! { #[allow(deprecated)] }
} else {
quote! {}
};
quote! {
#attrs
#[repr(transparent)]
#vis struct #name(i64);
#allow_deprecated
impl #name {
#(#bits)*
#vis const DECLARED_MASK: i64 = #mask_lit;
#vis const fn get(self) -> i64 { self.0 }
}
#allow_deprecated
impl ::core::convert::TryFrom<i64> for #name {
type Error = ::ridl_rt::payload::Violation;
fn try_from(value: i64) -> ::core::result::Result<Self, Self::Error> {
if value & !Self::DECLARED_MASK != 0 {
return ::core::result::Result::Err(::ridl_rt::payload::Violation {
type_name: #type_name,
rule: ::ridl_rt::payload::Rule::Variant,
});
}
::core::result::Result::Ok(Self(value))
}
}
#allow_deprecated
impl ::core::convert::From<#name> for i64 {
fn from(value: #name) -> Self { value.0 }
}
}
}
fn emit_union(decl: &v2::Decl, ud: &v2::UnionDef, derived: &TokenStream) -> TokenStream {
let name = ident(&decl.name);
let attrs = decl_attrs(decl, derived);
let vis = vis_tokens(decl.visibility);
let variants = ud.arms.iter().map(|arm| {
let vname = ident(&camel_case(&arm.name));
let ty = type_path(&arm.type_ref);
let vdoc = doc_attrs(&arm.doc);
quote! { #vdoc #vname(#ty) }
});
quote! {
#attrs
#vis enum #name {
#(#variants),*
}
}
}
fn emit_tuple_struct(
ctx: &Ctx,
induced: &InducedTuple,
tuples: &mut Vec<InducedTuple>,
) -> TokenStream {
let InducedTuple {
name,
tuple,
visibility,
} = induced;
let name_id = ident(name);
let vis = vis_tokens(*visibility);
let derived = derives::tuple_derive_attr(ctx, tuple);
let fields = tuple.fields.iter().map(|field| {
let fname = ident(&snake_case(&field.name));
let hint = format!("{}{}", name, camel_case(&field.name));
let ty = field
.r#type
.as_ref()
.map(|ft| field_type_tokens(ft, &hint, *visibility, tuples))
.unwrap_or_else(|| quote! { () });
quote! { pub #fname: #ty }
});
let struct_item = quote! {
#derived
#vis struct #name_id {
#(#fields),*
}
};
let default_impl = defaults::tuple_default_expr(ctx, name, tuple)
.map(|expr| quote! { impl Default for #name_id { fn default() -> Self { #expr } } })
.unwrap_or_default();
quote! { #struct_item #default_impl }
}
pub(crate) fn field_type_tokens(
ft: &v2::FieldType,
hint: &str,
visibility: i32,
tuples: &mut Vec<InducedTuple>,
) -> TokenStream {
let inner = match &ft.kind {
Some(v2::field_type::Kind::Named(name)) => type_path(name),
Some(v2::field_type::Kind::Primitive(prim)) => primitive_tokens(*prim),
Some(v2::field_type::Kind::InlineScalar(td)) => inline_scalar_tokens(td),
Some(v2::field_type::Kind::Tuple(tuple)) => {
let tuple_name = hint.to_string();
tuples.push(InducedTuple {
name: tuple_name.clone(),
tuple: tuple.clone(),
visibility,
});
let id = ident(&tuple_name);
quote! { #id }
}
Some(v2::field_type::Kind::Array(array)) => {
let element = array
.element
.as_ref()
.map(|el| field_type_tokens(el, &format!("{hint}Element"), visibility, tuples))
.unwrap_or_else(|| quote! { () });
if array.min == array.max {
let len = usize_tokens(array.min);
quote! { [#element; #len] }
} else {
quote! { Vec<#element> }
}
}
Some(v2::field_type::Kind::Map(map)) => {
let key = map
.key
.as_ref()
.map(|k| field_type_tokens(k, &format!("{hint}Key"), visibility, tuples))
.unwrap_or_else(|| quote! { () });
let value = map
.value
.as_ref()
.map(|v| field_type_tokens(v, &format!("{hint}Value"), visibility, tuples))
.unwrap_or_else(|| quote! { () });
quote! { Vec<(#key, #value)> }
}
Some(v2::field_type::Kind::Stream(_)) | None => quote! { () },
};
if ft.optional {
quote! { Option<#inner> }
} else {
inner
}
}
fn newtype_inner(td: &v2::TypeDef) -> TokenStream {
match backing_scalar(td) {
ScalarBacking::Float => quote! { f64 },
ScalarBacking::Integer => quote! { i64 },
ScalarBacking::Boolean => quote! { bool },
ScalarBacking::String => quote! { String },
ScalarBacking::Bytes => quote! { Vec<u8> },
}
}
fn inline_scalar_tokens(td: &v2::TypeDef) -> TokenStream {
match backing_scalar(td) {
ScalarBacking::Float => quote! { f64 },
ScalarBacking::Integer => quote! { i64 },
ScalarBacking::Boolean => quote! { bool },
ScalarBacking::String => quote! { String },
ScalarBacking::Bytes => quote! { Vec<u8> },
}
}
pub(crate) fn primitive_tokens(prim: i32) -> TokenStream {
match v2::PrimitiveType::try_from(prim).unwrap_or(v2::PrimitiveType::Unspecified) {
v2::PrimitiveType::Boolean => quote! { bool },
v2::PrimitiveType::Integer => quote! { i64 },
v2::PrimitiveType::Float => quote! { f64 },
v2::PrimitiveType::String => quote! { String },
v2::PrimitiveType::Bytes => quote! { Vec<u8> },
v2::PrimitiveType::Unspecified => quote! { () },
}
}
pub(crate) fn type_path(reference: &str) -> TokenStream {
if reference.contains('.') {
let segments = reference.split('.').map(ident);
quote! { crate #(:: #segments)* }
} else {
let id = ident(reference);
quote! { #id }
}
}
pub fn module_segment(segment: &str) -> String {
ident(segment).to_string()
}
fn strip_regex_delimiters(regex: &str) -> &str {
regex
.strip_prefix('/')
.and_then(|rest| rest.strip_suffix('/'))
.unwrap_or(regex)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScalarBacking {
Float,
Integer,
Boolean,
String,
Bytes,
}
pub(crate) fn backing_scalar(td: &v2::TypeDef) -> ScalarBacking {
match td.backing.as_ref().and_then(|b| b.kind.as_ref()) {
Some(v2::backing::Kind::Unit(_)) => ScalarBacking::Float,
Some(v2::backing::Kind::Primitive(prim)) => {
match v2::PrimitiveType::try_from(*prim).unwrap_or(v2::PrimitiveType::Unspecified) {
v2::PrimitiveType::Boolean => ScalarBacking::Boolean,
v2::PrimitiveType::Integer => ScalarBacking::Integer,
v2::PrimitiveType::Float => ScalarBacking::Float,
v2::PrimitiveType::String => ScalarBacking::String,
v2::PrimitiveType::Bytes | v2::PrimitiveType::Unspecified => ScalarBacking::Bytes,
}
}
None => ScalarBacking::Float,
}
}
pub(crate) fn same_package_scalar_backing(ctx: &Ctx, reference: &str) -> Option<ScalarBacking> {
match &ctx.lookup(reference)?.kind {
Some(v2::decl::Kind::TypeDef(td)) => Some(backing_scalar(td)),
_ => None,
}
}
fn same_package_scalar_ctor(ctx: &Ctx, reference: &str) -> Option<TokenStream> {
match &ctx.lookup(reference)?.kind {
Some(v2::decl::Kind::TypeDef(td)) => Some(scalar_ctor(td)),
_ => None,
}
}
fn primitive_keyword(reference: &str) -> Option<v2::PrimitiveType> {
match reference {
"boolean" => Some(v2::PrimitiveType::Boolean),
"integer" => Some(v2::PrimitiveType::Integer),
"float" => Some(v2::PrimitiveType::Float),
"string" => Some(v2::PrimitiveType::String),
"bytes" => Some(v2::PrimitiveType::Bytes),
_ => None,
}
}
fn decl_attrs(decl: &v2::Decl, derived: &TokenStream) -> TokenStream {
let doc = doc_attrs(&decl.doc);
let deprecated = deprecated_attr(decl.deprecated.as_deref());
quote! { #doc #derived #deprecated }
}
fn field_attrs(field: &v2::Field) -> TokenStream {
let doc = doc_attrs(&field.doc);
let deprecated = deprecated_attr(field.deprecated.as_deref());
quote! { #doc #deprecated }
}
pub(crate) fn doc_attrs(doc: &str) -> TokenStream {
if doc.is_empty() {
return quote! {};
}
let lines = doc.split('\n').map(|line| {
let text = format!(" {line}");
quote! { #[doc = #text] }
});
quote! { #(#lines)* }
}
pub(crate) fn deprecated_attr(reason: Option<&str>) -> TokenStream {
match reason {
Some("") => quote! { #[deprecated] },
Some(reason) => quote! { #[deprecated(note = #reason)] },
None => quote! {},
}
}
pub(crate) fn vis_tokens(visibility: i32) -> TokenStream {
match v2::Visibility::try_from(visibility).unwrap_or(v2::Visibility::Unspecified) {
v2::Visibility::Internal => quote! { pub(crate) },
_ => quote! { pub },
}
}
pub(crate) fn ident(name: &str) -> Ident {
if let Ok(parsed) = syn::parse_str::<Ident>(name) {
return parsed;
}
if matches!(name, "crate" | "self" | "Self" | "super" | "_") {
return Ident::new(&format!("{name}_"), Span::call_site());
}
if name.is_empty() {
return Ident::new("_", Span::call_site());
}
Ident::new_raw(name, Span::call_site())
}
pub(crate) fn numeric_tokens(value: &str, is_float: bool) -> TokenStream {
let text = if is_float && !value.contains('.') && !value.contains('e') && !value.contains('E') {
format!("{value}.0")
} else {
value.to_string()
};
text.parse().unwrap_or_else(|_| quote! { 0 })
}
fn int_tokens(value: i64) -> TokenStream {
value.to_string().parse().unwrap_or_else(|_| quote! { 0 })
}
fn usize_tokens(value: u64) -> TokenStream {
value.to_string().parse().unwrap_or_else(|_| quote! { 0 })
}
pub(crate) fn bool_tokens(value: &str) -> TokenStream {
if value == "true" {
quote! { true }
} else {
quote! { false }
}
}
#[cfg(test)]
mod tests;