//! 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), /// `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::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")) } } 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", _ => 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, } } 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()) { ("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::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)?))) } 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); } } } } } } } Err("unsupported `impl Trait` type".into()) } 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::Ptr(_) => "raw pointer", Type::TraitObject(_) => "trait object", Type::Never(_) => "never", Type::Macro(_) => "macro", _ => "other", } }