//! Rust type -> Nim type mapping. //! //! Integer width is preserved exactly. Anything that cannot be represented //! faithfully in Nim is reported as an error rather than approximated: a //! silently widened integer would change the meaning of wrapping arithmetic, //! which is precisely the kind of code people write in Rust. use syn::{GenericArgument, PathArguments, Type, TypeParamBound}; #[derive(Debug, Clone, PartialEq)] pub enum Nim { Prim(String), Seq(Box), OpenArray(Box), Array(usize, Box), Tuple(Vec), Named(String, Vec), Var(Box), /// `*mut T`. Nim's `ptr` is the same thing: an unmanaged address. Ptr(Box), /// `*const T`. The const matters at the C level even though it does not at /// the Rust one: Nim emits a real prototype where Rust emits none, and a /// `char *` declaration against C's `const char *` is a compile error. ConstPtr(Box), /// `impl Fn(A) -> B` / `fn(A) -> B`. Left at Nim's default calling /// convention (`closure`), which accepts both a plain top-level proc and /// a closure that captures -- and Rust's `impl Fn` accepts both too. Proc(Vec, Box), Unit, } impl Nim { pub fn render(&self) -> String { match self { Nim::Prim(s) => s.clone(), Nim::Seq(t) => format!("seq[{}]", t.render()), Nim::OpenArray(t) => format!("openArray[{}]", t.render()), Nim::Array(n, t) => format!("array[{}, {}]", n, t.render()), Nim::Tuple(ts) => { let inner: Vec = ts.iter().map(|t| t.render()).collect(); format!("({})", inner.join(", ")) } Nim::Named(n, args) if args.is_empty() => n.clone(), Nim::Named(n, args) => { let inner: Vec = args.iter().map(|t| t.render()).collect(); format!("{}[{}]", n, inner.join(", ")) } Nim::Var(t) => format!("var {}", t.render()), Nim::Ptr(t) => match &**t { Nim::Prim(p) if p == "void" => "pointer".into(), inner => format!("ptr {}", inner.render()), }, Nim::ConstPtr(t) => match &**t { Nim::Prim(p) if p == "void" => "pointer".into(), inner => const_ptr_alias(inner), }, Nim::Proc(args, ret) => { // Nim's proc types name their parameters even when the name is // never used. let inner: Vec = args .iter() .enumerate() .map(|(i, t)| format!("a{}: {}", i, t.render())) .collect(); match &**ret { Nim::Unit => format!("proc ({})", inner.join(", ")), r => format!("proc ({}): {}", inner.join(", "), r.render()), } } Nim::Unit => "void".into(), } } /// Strip a `var`, which is a parameter-passing mode rather than a type. /// Unlike `owned`, this keeps a view a view. pub fn unvar(self) -> Nim { match self { Nim::Var(t) => t.unvar(), other => other, } } /// Owned form: a borrowed slice parameter is `openArray[T]`, but the same /// type in an owned position (a field, a return value) must be `seq[T]`. pub fn owned(self) -> Nim { match self { Nim::OpenArray(t) => Nim::Seq(t), Nim::Var(t) => t.owned(), other => other, } } pub fn is_integer(&self) -> bool { matches!(self, Nim::Prim(p) if matches!(p.as_str(), "int8"|"int16"|"int32"|"int64"|"int"| "uint8"|"uint16"|"uint32"|"uint64"|"uint")) } pub fn is_unsigned(&self) -> bool { matches!(self, Nim::Prim(p) if p.starts_with("uint")) } } /// C types, as `libc` spells them. Nim has the same set under its own names, /// and both are defined by the platform's C compiler, so these are equal by /// construction rather than by assumption. pub fn c_type(name: &str) -> Option<&'static str> { Some(match name { "c_char" => "cchar", "c_schar" => "cschar", "c_uchar" => "cuchar", "c_short" => "cshort", "c_ushort" => "cushort", "c_int" => "cint", "c_uint" => "cuint", "c_long" => "clong", "c_ulong" => "culong", "c_longlong" => "clonglong", "c_ulonglong" => "culonglong", "c_float" => "cfloat", "c_double" => "cdouble", "size_t" => "csize_t", "ssize_t" => "int", "intptr_t" => "int", "uintptr_t" => "uint", "c_void" => "void", _ => return None, }) } pub fn prim(name: &str) -> Option { let mapped = match name { "i8" => "int8", "i16" => "int16", "i32" => "int32", "i64" => "int64", "isize" => "int", "u8" => "uint8", "u16" => "uint16", "u32" => "uint32", "u64" => "uint64", "usize" => "uint", "f32" => "float32", "f64" => "float64", "bool" => "bool", "char" => "Rune", "str" | "String" => "string", other => match c_type(other) { Some(c) => c, None => return None, }, }; Some(Nim::Prim(mapped.into())) } /// Types we refuse rather than approximate. pub fn rejected(name: &str) -> Option<&'static str> { match name { "i128" | "u128" => Some("128-bit integers have no faithful Nim equivalent"), _ => None, } } /// The Nim type name for a const-qualified C pointer to `t`. pub fn const_ptr_alias(t: &Nim) -> String { format!("RsConstPtr{}", t.render().replace(' ', "")) } /// The C spelling of a Nim type, for a const-qualified pointer declaration. pub fn c_spelling(t: &Nim) -> Option<&'static str> { let Nim::Prim(p) = t else { return None }; Some(match p.as_str() { "cchar" => "char", "cschar" => "signed char", "cuchar" => "unsigned char", "cshort" => "short", "cushort" => "unsigned short", "cint" => "int", "cuint" => "unsigned int", "clong" => "long", "culong" => "unsigned long", "clonglong" => "long long", "culonglong" => "unsigned long long", "cfloat" => "float", "cdouble" => "double", "uint8" => "unsigned char", "int8" => "signed char", "uint16" => "unsigned short", "int16" => "short", "uint32" => "unsigned int", "int32" => "int", "uint64" => "unsigned long long", "int64" => "long long", _ => return None, }) } fn ret_ty(r: &syn::ReturnType) -> Result { match r { syn::ReturnType::Default => Ok(Nim::Unit), syn::ReturnType::Type(_, t) => Ok(map(t)?.owned()), } } /// Types from `core::fmt` that must not be confused with a user type of the /// same short name. `fmt::Error` and a crate's own `Error` are different /// types, and collapsing a path to its last segment would merge them. fn std_qualified(p: &syn::Path) -> Option { let segs: Vec = p.segments.iter().map(|s| s.ident.to_string()).collect(); if segs.len() < 2 { return None; } let (q, name) = (&segs[segs.len() - 2], segs.last()?.as_str()); if q != "fmt" { return None; } Some(Nim::Prim( match name { "Error" => "FmtError", "Formatter" => "Formatter", // `fmt::Result` is `Result<(), fmt::Error>`. A formatting impl is // lowered to a proc that returns the formatted string, so the // result type is erased there; this spelling exists so that a // signature mentioning it still maps to something. "Result" => "FmtResult", _ => return None, } .into(), )) } pub fn map(t: &Type) -> Result { match t { Type::Path(p) => { if let Some(n) = std_qualified(&p.path) { return Ok(n); } let seg = p .path .segments .last() .ok_or_else(|| "empty type path".to_string())?; let name = seg.ident.to_string(); if let Some(why) = rejected(&name) { return Err(format!("unsupported type `{}`: {}", name, why)); } let args: Vec = match &seg.arguments { PathArguments::AngleBracketed(a) => a .args .iter() .filter_map(|g| match g { GenericArgument::Type(t) => Some(map(t)), _ => None, }) .collect::>()?, _ => vec![], }; match (name.as_str(), args.len()) { // The `log` facade's types, under shim names. ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())), ("LevelFilter", 0) => Ok(Nim::Prim("RsLogFilter".into())), ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))), ("Option", 1) => Ok(Nim::Named("Option".into(), args)), ("Result", 2) => Ok(Nim::Named("Result".into(), args)), ("Box", 1) => Ok(args[0].clone()), _ => { if let Some(p) = prim(&name) { Ok(p) } else { Ok(Nim::Named(name, args)) } } } } // &T is a value in Nim; &mut T becomes a `var` parameter. The caller // decides whether a `var` is legal in the position it is used. Type::Reference(r) => { // `&str` is a borrowed view of characters, not an owned string. // Nim accepts a `string` argument for an `openArray[char]` // parameter, so a literal still passes straight through. if let Type::Path(p) = &*r.elem { if p.path.is_ident("str") { return Ok(Nim::OpenArray(Box::new(Nim::Prim("char".into())))); } } let inner = map(&r.elem)?; if r.mutability.is_some() { Ok(Nim::Var(Box::new(inner))) } else { Ok(inner) } } Type::Ptr(p) => { let inner = Box::new(map(&p.elem)?); Ok(if matches!(p.mutability, syn::PointerMutability::Mut(_)) { Nim::Ptr(inner) } else { Nim::ConstPtr(inner) }) } Type::Slice(s) => Ok(Nim::OpenArray(Box::new(map(&s.elem)?))), Type::Array(a) => { let len = match &a.len { syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => i .base10_parse::() .map_err(|e| format!("array length: {}", e))?, _ => return Err("array length must be a literal".into()), }; Ok(Nim::Array(len, Box::new(map(&a.elem)?))) } Type::Tuple(t) if t.elems.is_empty() => Ok(Nim::Unit), Type::Tuple(t) => Ok(Nim::Tuple( t.elems.iter().map(map).collect::>()?, )), Type::Paren(p) => map(&p.elem), Type::Group(g) => map(&g.elem), Type::FnPtr(f) => { let args: Vec = f .inputs .iter() .map(|a| map(&a.ty)) .collect::>()?; Ok(Nim::Proc(args, Box::new(ret_ty(&f.output)?))) } // `dyn Fn(A) -> B` is a callable, exactly as `impl Fn(A) -> B` is. // Other trait objects need a vtable, which the lowering builds from // the trait's declaration; `dyn` is spelled `Dyn` there. Type::TraitObject(t) => { for b in &t.bounds { if let TypeParamBound::Trait(tb) = b { if let Some(seg) = tb.path.segments.last() { let n = seg.ident.to_string(); if n == "Fn" || n == "FnMut" || n == "FnOnce" { if let PathArguments::Parenthesized(a) = &seg.arguments { let args: Vec = a .inputs .iter() .map(|a| map(&a.ty)) .collect::>()?; return Ok(Nim::Proc(args, Box::new(ret_ty(&a.output)?))); } } // `dyn T + Send + Sync`: the auto traits carry no // methods, so the first real bound names the object. if !matches!(n.as_str(), "Send" | "Sync" | "Unpin" | "Sized") { return Ok(Nim::Named(format!("{n}Dyn"), vec![])); } } } } Err("a trait object with no nameable trait bound".into()) } Type::ImplTrait(i) => { // `impl AsRef<[u8]>` and friends: fall back to the bound's own // shape where we can recognise it, since Nim has no impl-trait. for b in &i.bounds { if let TypeParamBound::Trait(tb) = b { if let Some(seg) = tb.path.segments.last() { // `impl Fn(A) -> B` is a callable; Nim has a proc type // for exactly this. if seg.ident == "Fn" || seg.ident == "FnMut" || seg.ident == "FnOnce" { if let PathArguments::Parenthesized(a) = &seg.arguments { let args: Vec = a .inputs .iter() .map(|a| map(&a.ty)) .collect::>()?; return Ok(Nim::Proc(args, Box::new(ret_ty(&a.output)?))); } } if seg.ident == "AsRef" || seg.ident == "Into" { if let PathArguments::AngleBracketed(a) = &seg.arguments { for g in &a.args { if let GenericArgument::Type(t) = g { return map(t); } } } } } } } // Named so the message says which trait and, by implication, that // the caller is in return position -- argument position is handled // by the lowering, which turns it into a generic parameter. let named = i .bounds .iter() .find_map(|b| match b { TypeParamBound::Trait(tb) => { tb.path.segments.last().map(|s| s.ident.to_string()) } _ => None, }) .unwrap_or_else(|| "?".into()); Err(format!( "`impl {named}` in return position is an opaque type: the caller \ cannot name it, and Nim has no equivalent. In argument position \ `impl {named}` lowers fine, as the generic parameter it is" )) } Type::Infer(_) => Err("inferred type in a position that needs a name".into()), other => Err(format!("unsupported type form: {:?}", discriminant(other))), } } fn discriminant(t: &Type) -> &'static str { match t { Type::TraitObject(_) => "trait object", Type::Never(_) => "never", Type::Macro(_) => "macro", _ => "other", } }