| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 1 | //! Rust type -> Nim type mapping. |
| 2 | //! |
| 3 | //! Integer width is preserved exactly. Anything that cannot be represented |
| 4 | //! faithfully in Nim is reported as an error rather than approximated: a |
| 5 | //! silently widened integer would change the meaning of wrapping arithmetic, |
| 6 | //! which is precisely the kind of code people write in Rust. |
| 7 | |
| 8 | use syn::{GenericArgument, PathArguments, Type, TypeParamBound}; |
| 9 | |
| 10 | #[derive(Debug, Clone, PartialEq)] |
| 11 | pub enum Nim { |
| 12 | Prim(String), |
| 13 | Seq(Box<Nim>), |
| 14 | OpenArray(Box<Nim>), |
| 15 | Array(usize, Box<Nim>), |
| 16 | Tuple(Vec<Nim>), |
| 17 | Named(String, Vec<Nim>), |
| 18 | Var(Box<Nim>), |
| Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 6h ago | 19 | /// `*mut T`. Nim's `ptr` is the same thing: an unmanaged address. |
| 20 | Ptr(Box<Nim>), |
| 21 | /// `*const T`. The const matters at the C level even though it does not at |
| 22 | /// the Rust one: Nim emits a real prototype where Rust emits none, and a |
| 23 | /// `char *` declaration against C's `const char *` is a compile error. |
| 24 | ConstPtr(Box<Nim>), |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 9h ago | 25 | /// `impl Fn(A) -> B` / `fn(A) -> B`. Left at Nim's default calling |
| 26 | /// convention (`closure`), which accepts both a plain top-level proc and |
| 27 | /// a closure that captures -- and Rust's `impl Fn` accepts both too. |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 9h ago | 28 | Proc(Vec<Nim>, Box<Nim>), |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 29 | Unit, |
| 30 | } |
| 31 | |
| 32 | impl Nim { |
| 33 | pub fn render(&self) -> String { |
| 34 | match self { |
| 35 | Nim::Prim(s) => s.clone(), |
| 36 | Nim::Seq(t) => format!("seq[{}]", t.render()), |
| 37 | Nim::OpenArray(t) => format!("openArray[{}]", t.render()), |
| 38 | Nim::Array(n, t) => format!("array[{}, {}]", n, t.render()), |
| 39 | Nim::Tuple(ts) => { |
| 40 | let inner: Vec<String> = ts.iter().map(|t| t.render()).collect(); |
| 41 | format!("({})", inner.join(", ")) |
| 42 | } |
| 43 | Nim::Named(n, args) if args.is_empty() => n.clone(), |
| 44 | Nim::Named(n, args) => { |
| 45 | let inner: Vec<String> = args.iter().map(|t| t.render()).collect(); |
| 46 | format!("{}[{}]", n, inner.join(", ")) |
| 47 | } |
| 48 | Nim::Var(t) => format!("var {}", t.render()), |
| Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 6h ago | 49 | Nim::Ptr(t) => match &**t { |
| 50 | Nim::Prim(p) if p == "void" => "pointer".into(), |
| 51 | inner => format!("ptr {}", inner.render()), |
| 52 | }, |
| 53 | Nim::ConstPtr(t) => match &**t { |
| 54 | Nim::Prim(p) if p == "void" => "pointer".into(), |
| 55 | inner => const_ptr_alias(inner), |
| 56 | }, |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 9h ago | 57 | Nim::Proc(args, ret) => { |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 9h ago | 58 | // Nim's proc types name their parameters even when the name is |
| 59 | // never used. |
| 60 | let inner: Vec<String> = args |
| 61 | .iter() |
| 62 | .enumerate() |
| 63 | .map(|(i, t)| format!("a{}: {}", i, t.render())) |
| 64 | .collect(); |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 9h ago | 65 | match &**ret { |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 9h ago | 66 | Nim::Unit => format!("proc ({})", inner.join(", ")), |
| 67 | r => format!("proc ({}): {}", inner.join(", "), r.render()), |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 9h ago | 68 | } |
| 69 | } |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 70 | Nim::Unit => "void".into(), |
| 71 | } |
| 72 | } |
| 73 | |
| Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 8h ago | 74 | /// Strip a `var`, which is a parameter-passing mode rather than a type. |
| 75 | /// Unlike `owned`, this keeps a view a view. |
| 76 | pub fn unvar(self) -> Nim { |
| 77 | match self { |
| 78 | Nim::Var(t) => t.unvar(), |
| 79 | other => other, |
| 80 | } |
| 81 | } |
| 82 | |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 83 | /// Owned form: a borrowed slice parameter is `openArray[T]`, but the same |
| 84 | /// type in an owned position (a field, a return value) must be `seq[T]`. |
| 85 | pub fn owned(self) -> Nim { |
| 86 | match self { |
| 87 | Nim::OpenArray(t) => Nim::Seq(t), |
| 88 | Nim::Var(t) => t.owned(), |
| 89 | other => other, |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | pub fn is_integer(&self) -> bool { |
| 94 | matches!(self, Nim::Prim(p) if matches!(p.as_str(), |
| 95 | "int8"|"int16"|"int32"|"int64"|"int"| |
| 96 | "uint8"|"uint16"|"uint32"|"uint64"|"uint")) |
| 97 | } |
| 98 | |
| 99 | pub fn is_unsigned(&self) -> bool { |
| 100 | matches!(self, Nim::Prim(p) if p.starts_with("uint")) |
| 101 | } |
| 102 | } |
| 103 | |
| Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 6h ago | 104 | /// C types, as `libc` spells them. Nim has the same set under its own names, |
| 105 | /// and both are defined by the platform's C compiler, so these are equal by |
| 106 | /// construction rather than by assumption. |
| 107 | pub fn c_type(name: &str) -> Option<&'static str> { |
| 108 | Some(match name { |
| 109 | "c_char" => "cchar", |
| 110 | "c_schar" => "cschar", |
| 111 | "c_uchar" => "cuchar", |
| 112 | "c_short" => "cshort", |
| 113 | "c_ushort" => "cushort", |
| 114 | "c_int" => "cint", |
| 115 | "c_uint" => "cuint", |
| 116 | "c_long" => "clong", |
| 117 | "c_ulong" => "culong", |
| 118 | "c_longlong" => "clonglong", |
| 119 | "c_ulonglong" => "culonglong", |
| 120 | "c_float" => "cfloat", |
| 121 | "c_double" => "cdouble", |
| 122 | "size_t" => "csize_t", |
| 123 | "ssize_t" => "int", |
| 124 | "intptr_t" => "int", |
| 125 | "uintptr_t" => "uint", |
| 126 | "c_void" => "void", |
| 127 | _ => return None, |
| 128 | }) |
| 129 | } |
| 130 | |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 131 | pub fn prim(name: &str) -> Option<Nim> { |
| 132 | let mapped = match name { |
| 133 | "i8" => "int8", |
| 134 | "i16" => "int16", |
| 135 | "i32" => "int32", |
| 136 | "i64" => "int64", |
| 137 | "isize" => "int", |
| 138 | "u8" => "uint8", |
| 139 | "u16" => "uint16", |
| 140 | "u32" => "uint32", |
| 141 | "u64" => "uint64", |
| 142 | "usize" => "uint", |
| 143 | "f32" => "float32", |
| 144 | "f64" => "float64", |
| 145 | "bool" => "bool", |
| 146 | "char" => "Rune", |
| 147 | "str" | "String" => "string", |
| Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 6h ago | 148 | other => match c_type(other) { |
| 149 | Some(c) => c, |
| 150 | None => return None, |
| 151 | }, |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 152 | }; |
| 153 | Some(Nim::Prim(mapped.into())) |
| 154 | } |
| 155 | |
| 156 | /// Types we refuse rather than approximate. |
| 157 | pub fn rejected(name: &str) -> Option<&'static str> { |
| 158 | match name { |
| 159 | "i128" | "u128" => Some("128-bit integers have no faithful Nim equivalent"), |
| 160 | _ => None, |
| 161 | } |
| 162 | } |
| 163 | |
| Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 6h ago | 164 | /// The Nim type name for a const-qualified C pointer to `t`. |
| 165 | pub fn const_ptr_alias(t: &Nim) -> String { |
| 166 | format!("RsConstPtr{}", t.render().replace(' ', "")) |
| 167 | } |
| 168 | |
| 169 | /// The C spelling of a Nim type, for a const-qualified pointer declaration. |
| 170 | pub fn c_spelling(t: &Nim) -> Option<&'static str> { |
| 171 | let Nim::Prim(p) = t else { return None }; |
| 172 | Some(match p.as_str() { |
| 173 | "cchar" => "char", |
| 174 | "cschar" => "signed char", |
| 175 | "cuchar" => "unsigned char", |
| 176 | "cshort" => "short", |
| 177 | "cushort" => "unsigned short", |
| 178 | "cint" => "int", |
| 179 | "cuint" => "unsigned int", |
| 180 | "clong" => "long", |
| 181 | "culong" => "unsigned long", |
| 182 | "clonglong" => "long long", |
| 183 | "culonglong" => "unsigned long long", |
| 184 | "cfloat" => "float", |
| 185 | "cdouble" => "double", |
| 186 | "uint8" => "unsigned char", |
| 187 | "int8" => "signed char", |
| 188 | "uint16" => "unsigned short", |
| 189 | "int16" => "short", |
| 190 | "uint32" => "unsigned int", |
| 191 | "int32" => "int", |
| 192 | "uint64" => "unsigned long long", |
| 193 | "int64" => "long long", |
| 194 | _ => return None, |
| 195 | }) |
| 196 | } |
| 197 | |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 9h ago | 198 | fn ret_ty(r: &syn::ReturnType) -> Result<Nim, String> { |
| 199 | match r { |
| 200 | syn::ReturnType::Default => Ok(Nim::Unit), |
| 201 | syn::ReturnType::Type(_, t) => Ok(map(t)?.owned()), |
| 202 | } |
| 203 | } |
| 204 | |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 9h ago | 205 | /// Types from `core::fmt` that must not be confused with a user type of the |
| 206 | /// same short name. `fmt::Error` and a crate's own `Error` are different |
| 207 | /// types, and collapsing a path to its last segment would merge them. |
| 208 | fn std_qualified(p: &syn::Path) -> Option<Nim> { |
| 209 | let segs: Vec<String> = p.segments.iter().map(|s| s.ident.to_string()).collect(); |
| 210 | if segs.len() < 2 { |
| 211 | return None; |
| 212 | } |
| 213 | let (q, name) = (&segs[segs.len() - 2], segs.last()?.as_str()); |
| 214 | if q != "fmt" { |
| 215 | return None; |
| 216 | } |
| 217 | Some(Nim::Prim( |
| 218 | match name { |
| 219 | "Error" => "FmtError", |
| 220 | "Formatter" => "Formatter", |
| 221 | // `fmt::Result` is `Result<(), fmt::Error>`. A formatting impl is |
| 222 | // lowered to a proc that returns the formatted string, so the |
| 223 | // result type is erased there; this spelling exists so that a |
| 224 | // signature mentioning it still maps to something. |
| 225 | "Result" => "FmtResult", |
| 226 | _ => return None, |
| 227 | } |
| 228 | .into(), |
| 229 | )) |
| 230 | } |
| 231 | |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 232 | pub fn map(t: &Type) -> Result<Nim, String> { |
| 233 | match t { |
| 234 | Type::Path(p) => { |
| Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 9h ago | 235 | if let Some(n) = std_qualified(&p.path) { |
| 236 | return Ok(n); |
| 237 | } |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 238 | let seg = p |
| 239 | .path |
| 240 | .segments |
| 241 | .last() |
| 242 | .ok_or_else(|| "empty type path".to_string())?; |
| 243 | let name = seg.ident.to_string(); |
| 244 | |
| 245 | if let Some(why) = rejected(&name) { |
| 246 | return Err(format!("unsupported type `{}`: {}", name, why)); |
| 247 | } |
| 248 | |
| 249 | let args: Vec<Nim> = match &seg.arguments { |
| 250 | PathArguments::AngleBracketed(a) => a |
| 251 | .args |
| 252 | .iter() |
| 253 | .filter_map(|g| match g { |
| 254 | GenericArgument::Type(t) => Some(map(t)), |
| 255 | _ => None, |
| 256 | }) |
| 257 | .collect::<Result<_, _>>()?, |
| 258 | _ => vec![], |
| 259 | }; |
| 260 | |
| 261 | match (name.as_str(), args.len()) { |
| Lower the `log` facade, and add explicit enum discriminants 12c0a01 nandithebull 6h ago | 262 | // The `log` facade's types, under shim names. |
| 263 | ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())), |
| 264 | ("LevelFilter", 0) => Ok(Nim::Prim("RsLogFilter".into())), |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 265 | ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))), |
| 266 | ("Option", 1) => Ok(Nim::Named("Option".into(), args)), |
| 267 | ("Result", 2) => Ok(Nim::Named("Result".into(), args)), |
| 268 | ("Box", 1) => Ok(args[0].clone()), |
| 269 | _ => { |
| 270 | if let Some(p) = prim(&name) { |
| 271 | Ok(p) |
| 272 | } else { |
| 273 | Ok(Nim::Named(name, args)) |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | } |
| 278 | // &T is a value in Nim; &mut T becomes a `var` parameter. The caller |
| 279 | // decides whether a `var` is legal in the position it is used. |
| 280 | Type::Reference(r) => { |
| Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 9h ago | 281 | // `&str` is a borrowed view of characters, not an owned string. |
| 282 | // Nim accepts a `string` argument for an `openArray[char]` |
| 283 | // parameter, so a literal still passes straight through. |
| 284 | if let Type::Path(p) = &*r.elem { |
| 285 | if p.path.is_ident("str") { |
| 286 | return Ok(Nim::OpenArray(Box::new(Nim::Prim("char".into())))); |
| 287 | } |
| 288 | } |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 289 | let inner = map(&r.elem)?; |
| 290 | if r.mutability.is_some() { |
| 291 | Ok(Nim::Var(Box::new(inner))) |
| 292 | } else { |
| 293 | Ok(inner) |
| 294 | } |
| 295 | } |
| Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 6h ago | 296 | Type::Ptr(p) => { |
| 297 | let inner = Box::new(map(&p.elem)?); |
| 298 | Ok(if matches!(p.mutability, syn::PointerMutability::Mut(_)) { |
| 299 | Nim::Ptr(inner) |
| 300 | } else { |
| 301 | Nim::ConstPtr(inner) |
| 302 | }) |
| 303 | } |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 304 | Type::Slice(s) => Ok(Nim::OpenArray(Box::new(map(&s.elem)?))), |
| 305 | Type::Array(a) => { |
| 306 | let len = match &a.len { |
| 307 | syn::Expr::Lit(syn::ExprLit { |
| 308 | lit: syn::Lit::Int(i), |
| 309 | .. |
| 310 | }) => i |
| 311 | .base10_parse::<usize>() |
| 312 | .map_err(|e| format!("array length: {}", e))?, |
| 313 | _ => return Err("array length must be a literal".into()), |
| 314 | }; |
| 315 | Ok(Nim::Array(len, Box::new(map(&a.elem)?))) |
| 316 | } |
| 317 | Type::Tuple(t) if t.elems.is_empty() => Ok(Nim::Unit), |
| 318 | Type::Tuple(t) => Ok(Nim::Tuple( |
| 319 | t.elems.iter().map(map).collect::<Result<_, _>>()?, |
| 320 | )), |
| 321 | Type::Paren(p) => map(&p.elem), |
| 322 | Type::Group(g) => map(&g.elem), |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 9h ago | 323 | Type::FnPtr(f) => { |
| 324 | let args: Vec<Nim> = f |
| 325 | .inputs |
| 326 | .iter() |
| 327 | .map(|a| map(&a.ty)) |
| 328 | .collect::<Result<_, _>>()?; |
| 329 | Ok(Nim::Proc(args, Box::new(ret_ty(&f.output)?))) |
| 330 | } |
| Lower `bitflags!` directly, checked against the real crate af6e50f nandithebull 6h ago | 331 | // `dyn Fn(A) -> B` is a callable, exactly as `impl Fn(A) -> B` is. |
| 332 | // Other trait objects need a vtable, which the lowering builds from |
| 333 | // the trait's declaration; `dyn` is spelled `<Trait>Dyn` there. |
| 334 | Type::TraitObject(t) => { |
| 335 | for b in &t.bounds { |
| 336 | if let TypeParamBound::Trait(tb) = b { |
| 337 | if let Some(seg) = tb.path.segments.last() { |
| 338 | let n = seg.ident.to_string(); |
| 339 | if n == "Fn" || n == "FnMut" || n == "FnOnce" { |
| 340 | if let PathArguments::Parenthesized(a) = &seg.arguments { |
| 341 | let args: Vec<Nim> = a |
| 342 | .inputs |
| 343 | .iter() |
| 344 | .map(|a| map(&a.ty)) |
| 345 | .collect::<Result<_, _>>()?; |
| 346 | return Ok(Nim::Proc(args, Box::new(ret_ty(&a.output)?))); |
| 347 | } |
| 348 | } |
| 349 | // `dyn T + Send + Sync`: the auto traits carry no |
| 350 | // methods, so the first real bound names the object. |
| 351 | if !matches!(n.as_str(), "Send" | "Sync" | "Unpin" | "Sized") { |
| 352 | return Ok(Nim::Named(format!("{n}Dyn"), vec![])); |
| 353 | } |
| 354 | } |
| 355 | } |
| 356 | } |
| 357 | Err("a trait object with no nameable trait bound".into()) |
| 358 | } |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 359 | Type::ImplTrait(i) => { |
| 360 | // `impl AsRef<[u8]>` and friends: fall back to the bound's own |
| 361 | // shape where we can recognise it, since Nim has no impl-trait. |
| 362 | for b in &i.bounds { |
| 363 | if let TypeParamBound::Trait(tb) = b { |
| 364 | if let Some(seg) = tb.path.segments.last() { |
| Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 9h ago | 365 | // `impl Fn(A) -> B` is a callable; Nim has a proc type |
| 366 | // for exactly this. |
| 367 | if seg.ident == "Fn" || seg.ident == "FnMut" || seg.ident == "FnOnce" { |
| 368 | if let PathArguments::Parenthesized(a) = &seg.arguments { |
| 369 | let args: Vec<Nim> = a |
| 370 | .inputs |
| 371 | .iter() |
| 372 | .map(|a| map(&a.ty)) |
| 373 | .collect::<Result<_, _>>()?; |
| 374 | return Ok(Nim::Proc(args, Box::new(ret_ty(&a.output)?))); |
| 375 | } |
| 376 | } |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 377 | if seg.ident == "AsRef" || seg.ident == "Into" { |
| 378 | if let PathArguments::AngleBracketed(a) = &seg.arguments { |
| 379 | for g in &a.args { |
| 380 | if let GenericArgument::Type(t) = g { |
| 381 | return map(t); |
| 382 | } |
| 383 | } |
| 384 | } |
| 385 | } |
| 386 | } |
| 387 | } |
| 388 | } |
| Lower argument-position `impl Trait`; it was a generic parameter all along 4e4d09d nandithebull 5h ago | 389 | // Named so the message says which trait and, by implication, that |
| 390 | // the caller is in return position -- argument position is handled |
| 391 | // by the lowering, which turns it into a generic parameter. |
| 392 | let named = i |
| 393 | .bounds |
| 394 | .iter() |
| 395 | .find_map(|b| match b { |
| 396 | TypeParamBound::Trait(tb) => { |
| 397 | tb.path.segments.last().map(|s| s.ident.to_string()) |
| 398 | } |
| 399 | _ => None, |
| 400 | }) |
| 401 | .unwrap_or_else(|| "?".into()); |
| 402 | Err(format!( |
| 403 | "`impl {named}` in return position is an opaque type: the caller \ |
| 404 | cannot name it, and Nim has no equivalent. In argument position \ |
| 405 | `impl {named}` lowers fine, as the generic parameter it is" |
| 406 | )) |
| Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 10h ago | 407 | } |
| 408 | Type::Infer(_) => Err("inferred type in a position that needs a name".into()), |
| 409 | other => Err(format!("unsupported type form: {:?}", discriminant(other))), |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | fn discriminant(t: &Type) -> &'static str { |
| 414 | match t { |
| 415 | Type::TraitObject(_) => "trait object", |
| 416 | Type::Never(_) => "never", |
| 417 | Type::Macro(_) => "macro", |
| 418 | _ => "other", |
| 419 | } |
| 420 | } |