//! Rust AST -> Nim source. //! //! The governing rule is in DESIGN.md and it shapes every function here: //! anything whose Rust semantics cannot be reproduced exactly in Nim returns //! `Err` with a reason. Nothing is emitted on a guess. Where a construct maps //! one-to-one (signed `shr`, unsigned wrapping, truncating `div`/`mod`) the //! mapping is direct and there is a comment saying why that is safe. use crate::fmt; use crate::ty::{self, Nim}; use std::collections::HashMap; use syn::{ BinOp, Expr, FnArg, Item, Lit, Local, Pat, ReturnType, Stmt, UnOp, }; // --------------------------------------------------------------- vocabulary /// Nim keywords. Rust code may legally use any of these as an identifier. const NIM_KEYWORDS: &[&str] = &[ "addr", "and", "as", "asm", "bind", "block", "break", "case", "cast", "concept", "const", "continue", "converter", "defer", "discard", "distinct", "div", "do", "elif", "else", "end", "enum", "except", "export", "finally", "for", "from", "func", "if", "import", "in", "include", "interface", "is", "isnot", "iterator", "let", "macro", "method", "mixin", "mod", "nil", "not", "notin", "object", "of", "or", "out", "proc", "ptr", "raise", "ref", "return", "shl", "shr", "static", "template", "try", "tuple", "type", "using", "var", "when", "while", "xor", "result", "echo", ]; fn ident(name: &str) -> String { if NIM_KEYWORDS.contains(&name) { format!("{name}_r") } else { name.to_string() } } /// A lowered expression: its Nim text, and its type where we know it. /// /// The type is not decoration. Nim needs it to pick `div` over `/`, to size a /// `cast`, and to annotate every binding so that Nim's own type checker /// catches a mistake in this file rather than letting it through as output /// that runs and is wrong. #[derive(Clone, Debug)] struct Val { code: String, ty: Option, } impl Val { fn new(code: impl Into, ty: Option) -> Self { Val { code: code.into(), ty } } fn untyped(code: impl Into) -> Self { Val { code: code.into(), ty: None } } } struct Sig { params: Vec, ret: Nim, } pub struct Lowerer { out: String, indent: usize, scopes: Vec>, fns: HashMap, /// struct name -> (field, type) structs: HashMap>, /// Return type of the proc being lowered, so `return e` and a trailing /// expression can type their literals the way Rust's inference would. ret: Option, /// `(name, type)` that the arms of the `if`/`match` being lowered as a /// statement must assign their value to. target: Option<(String, Option)>, tmp: usize, } impl Lowerer { pub fn new() -> Self { Lowerer { out: String::new(), indent: 0, scopes: vec![HashMap::new()], fns: HashMap::new(), structs: HashMap::new(), ret: None, target: None, tmp: 0, } } // ------------------------------------------------------------ emission fn line(&mut self, s: &str) { for _ in 0..self.indent { self.out.push_str(" "); } self.out.push_str(s); self.out.push('\n'); } fn blank(&mut self) { self.out.push('\n'); } fn fresh(&mut self, hint: &str) -> String { self.tmp += 1; format!("rsTmp{}{}", hint, self.tmp) } // --------------------------------------------------------------- scope fn push_scope(&mut self) { self.scopes.push(HashMap::new()); } fn pop_scope(&mut self) { self.scopes.pop(); } fn bind(&mut self, name: &str, t: Nim) { self.scopes.last_mut().unwrap().insert(name.to_string(), t); } fn lookup(&self, name: &str) -> Option { self.scopes.iter().rev().find_map(|s| s.get(name).cloned()) } // ---------------------------------------------------------------- file pub fn lower_file(&mut self, file: &syn::File) -> Result { self.out.push_str(include_str!("prelude.nim")); self.blank(); // Pass 1: signatures and struct shapes, so that a call can be typed // regardless of declaration order (Rust has no forward declarations). for item in &file.items { self.collect(item)?; } // Pass 2: bodies. for item in &file.items { self.item(item)?; } if self.fns.contains_key("main") { self.blank(); self.line("when isMainModule:"); self.indent += 1; self.line("try:"); self.line(" main()"); // Rust's panic exits 101 with a message on stderr. Nim's Defects // exit 1. Mapping them here is what keeps the differential runner's // exit-status comparison meaningful for panicking programs. self.line("except RustPanic as e:"); self.line(" stderr.writeLine(\"thread 'main' panicked: \" & e.msg)"); self.line(" quit(101)"); self.line("except Defect as e:"); self.line(" stderr.writeLine(\"thread 'main' panicked: \" & e.msg)"); self.line(" quit(101)"); self.indent -= 1; } Ok(std::mem::take(&mut self.out)) } fn collect(&mut self, item: &Item) -> Result<(), String> { match item { Item::Fn(f) => { let (params, ret) = self.signature(&f.sig)?; self.fns.insert(f.sig.ident.to_string(), Sig { params, ret }); } Item::Struct(s) => { let mut fields = Vec::new(); for (i, f) in s.fields.iter().enumerate() { let name = match &f.ident { Some(id) => id.to_string(), None => format!("f{i}"), // tuple struct }; fields.push((name, ty::map(&f.ty)?.owned())); } self.structs.insert(s.ident.to_string(), fields); } Item::Impl(im) => { let self_ty = ty::map(&im.self_ty)?; for it in &im.items { if let syn::ImplItem::Fn(m) = it { let (mut params, ret) = self.signature(&m.sig)?; if takes_self(&m.sig) { params.insert(0, self_ty.clone()); } self.fns.insert(m.sig.ident.to_string(), Sig { params, ret }); } } } _ => {} } Ok(()) } fn signature(&self, sig: &syn::Signature) -> Result<(Vec, Nim), String> { if sig.asyncness.is_some() { return Err(format!("`async fn {}`: Nim has no equivalent", sig.ident)); } if !sig.generics.params.is_empty() { return Err(format!( "`fn {}` is generic: generics are not implemented yet", sig.ident )); } let mut params = Vec::new(); for a in &sig.inputs { if let FnArg::Typed(t) = a { params.push(ty::map(&t.ty)?); } } let ret = match &sig.output { ReturnType::Default => Nim::Unit, ReturnType::Type(_, t) => ty::map(t)?.owned(), }; Ok((params, ret)) } // --------------------------------------------------------------- items fn item(&mut self, item: &Item) -> Result<(), String> { match item { Item::Fn(f) => self.func(&f.sig, &f.block, None), Item::Struct(s) => { let name = s.ident.to_string(); let fields = self.structs[&name].clone(); self.line(&format!("type {}* = object", ident(&name))); self.indent += 1; if fields.is_empty() { self.line("discard"); } for (fname, fty) in &fields { self.line(&format!("{}*: {}", ident(fname), fty.render())); } self.indent -= 1; self.blank(); Ok(()) } Item::Const(c) => { let t = ty::map(&c.ty)?.owned(); let v = self.expr(&c.expr)?; self.bind(&c.ident.to_string(), t.clone()); let line = format!("const {}*: {} = {}", ident(&c.ident.to_string()), t.render(), v.code); self.line(&line); self.blank(); Ok(()) } Item::Impl(im) => { let self_ty = ty::map(&im.self_ty)?; if im.trait_.is_some() { return Err(format!( "`impl Trait for {}`: trait impls are not implemented yet", self_ty.render() )); } for it in &im.items { match it { syn::ImplItem::Fn(m) => { let recv = if takes_self(&m.sig) { Some(self_ty.clone()) } else { None }; self.func(&m.sig, &m.block, recv)?; } _ => return Err("only `fn` items are supported inside `impl`".into()), } } Ok(()) } Item::Use(_) => Ok(()), // `use` has no Nim analogue in a single module Item::Mod(m) if m.content.is_none() => { Err(format!("`mod {};` (external file) is not implemented yet", m.ident)) } other => Err(format!("unsupported item: {}", item_kind(other))), } } fn func( &mut self, sig: &syn::Signature, body: &syn::Block, recv: Option, ) -> Result<(), String> { let name = sig.ident.to_string(); let (ptys, ret) = self.signature(sig)?; self.push_scope(); let mut rendered: Vec = Vec::new(); if let Some(self_ty) = recv { // `&mut self` and `mut self` both mean the body may mutate the // receiver; only the former is observable by the caller, and a Nim // `var` parameter is the faithful spelling of that. let mutable = matches!( sig.inputs.first(), Some(FnArg::Receiver(r)) if matches!(&r.kind, syn::ReceiverKind::Reference(_, _, m) if m.is_some()) ); let t = if mutable { Nim::Var(Box::new(self_ty.clone())) } else { self_ty.clone() }; rendered.push(format!("self: {}", t.render())); self.bind("self", self_ty); } let typed: Vec<&syn::PatType> = sig .inputs .iter() .filter_map(|a| match a { FnArg::Typed(t) => Some(t), _ => None, }) .collect(); for (p, t) in typed.iter().zip(ptys.iter()) { let pname = match &*p.pat { Pat::Ident(i) => i.ident.to_string(), _ => return Err("only plain identifier parameters are supported".into()), }; rendered.push(format!("{}: {}", ident(&pname), t.render())); // Inside the body a `var T` parameter is used exactly like a `T`. self.bind(&pname, t.clone().owned()); } let head = if ret == Nim::Unit { format!("proc {}*({}) =", ident(&name), rendered.join(", ")) } else { format!("proc {}*({}): {} =", ident(&name), rendered.join(", "), ret.render()) }; self.line(&head); self.indent += 1; let outer_ret = self.ret.replace(ret.clone()); // A Rust fn's trailing expression is its return value. Naming Nim's // implicit `result` as the target makes that true whether the tail is // a plain expression or an `if`/`match` with statement arms. let outer_target = if ret == Nim::Unit { self.target.take() } else { self.target.replace(("result".to_string(), Some(ret.clone()))) }; let before = self.out.len(); let tail = self.block_body_at(body, Some(&ret))?; self.target = outer_target; match tail { Some(v) if ret != Nim::Unit => { let code = v.code.clone(); self.line(&format!("result = {code}")); } Some(v) => { // A trailing expression in a `()`-returning fn is evaluated for // its effect; Nim requires an explicit discard. let needs_discard = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit); if needs_discard && !v.code.is_empty() { let code = v.code.clone(); self.line(&format!("discard {code}")); } } None => {} } if self.out.len() == before { self.line("discard"); } self.indent -= 1; self.ret = outer_ret; self.pop_scope(); self.blank(); Ok(()) } // ---------------------------------------------------------- statements /// Lower a block's statements. Returns the block's trailing expression, /// if it has one, *without* emitting it — the caller decides whether that /// value is a return value, a binding, or discarded. fn block_body(&mut self, b: &syn::Block) -> Result, String> { self.block_body_at(b, None) } fn block_body_at( &mut self, b: &syn::Block, expect: Option<&Nim>, ) -> Result, String> { // An assignment target belongs to *this* block's trailing expression // only. A non-final `if` is a statement and must not assign anything. let target = self.target.take(); let n = b.stmts.len(); let mut tail = None; for (i, st) in b.stmts.iter().enumerate() { let last = i + 1 == n; match st { Stmt::Expr(e, None) if last && expressible(e) => { tail = Some(self.expr_at(e, expect)?) } Stmt::Expr(e, None) if last => { // A trailing `if`/`match` with statement arms, or a loop. // Lower it as statements; if this block's value is wanted, // each arm assigns it. match &target { Some((t, ty)) => { let (t, ty) = (t.clone(), ty.clone()); self.assign_from(e, &t, ty.as_ref())?; } None => self.stmt(st)?, } } _ => self.stmt(st)?, } } self.target = target; Ok(tail) } /// Lower a block in statement position (loop bodies, `if` arms). fn nested_block(&mut self, b: &syn::Block) -> Result<(), String> { self.push_scope(); self.indent += 1; let before = self.out.len(); let want = self.target.clone().and_then(|(_, t)| t); let tail = self.block_body_at(b, want.as_ref())?; self.emit_tail(tail); if self.out.len() == before { self.line("discard"); } self.indent -= 1; self.pop_scope(); Ok(()) } fn stmt(&mut self, s: &Stmt) -> Result<(), String> { match s { Stmt::Local(l) => self.local(l), Stmt::Expr(e, _) => { let v = self.expr_stmt(e)?; if let Some(v) = v { // A bare expression with a value must be discarded in Nim. let needs = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit); let code = v.code.clone(); if needs { self.line(&format!("discard {code}")); } else if !code.is_empty() { self.line(&code); } } Ok(()) } Stmt::Item(i) => self.item(i), Stmt::Macro(m) => { let line = self.macro_call(&m.mac)?; self.line(&line); Ok(()) } } } fn local(&mut self, l: &Local) -> Result<(), String> { let (name, mutable, ann): (String, bool, Option) = match &l.pat { Pat::Ident(i) => (i.ident.to_string(), i.mutability.is_some(), None), Pat::Type(t) => match &*t.pat { Pat::Ident(i) => (i.ident.to_string(), i.mutability.is_some(), Some(ty::map(&t.ty)?)), _ => return Err("only `let ` bindings are supported".into()), }, Pat::Wild(_) => ("_".into(), false, None), _ => return Err("destructuring `let` is not implemented yet".into()), }; let Some(init) = &l.init else { // `let x: T;` — Nim's `var x: T` zero-initialises, which Rust does // not. Rust's own rules make reading it before assignment illegal, // so the two agree on every program rustc accepts. let t = ann.ok_or("`let` without an initialiser needs a type annotation")?; let t = t.owned(); self.line(&format!("var {}: {}", ident(&name), t.render())); self.bind(&name, t); return Ok(()); }; if init.diverge.is_some() { return Err("`let ... else` is not implemented yet".into()); } if !expressible(&init.expr) && name != "_" { // The initialiser is an `if`/`match` whose arms are statements. // Declare first, then let each arm assign into the binding. let t = ann .clone() .ok_or_else(|| { format!( "`let {name} = match/if ...` needs a type annotation: \ its arms are statements, so the binding must be \ declared before they run" ) })? .owned(); self.line(&format!("var {}: {}", ident(&name), t.render())); self.bind(&name, t.clone()); let target = ident(&name); return self.assign_from(&init.expr, &target, Some(&t)); } let v = self.expr_at(&init.expr, ann.as_ref())?; let t = match (ann, &v.ty) { (Some(a), _) => a.owned(), (None, Some(t)) => t.clone().owned(), (None, None) => { return Err(format!( "cannot infer the type of `let {name}`; annotate it — \ guessing here would change integer width, and with it the \ meaning of any arithmetic on `{name}`" )) } }; if name == "_" { let code = v.code.clone(); self.line(&format!("discard {code}")); return Ok(()); } // Rust's immutable `let` is Nim's `let`; `let mut` is `var`. Shadowing // works in both, so a re-`let` of the same name needs no rename. let kw = if mutable { "var" } else { "let" }; let line = format!("{} {}: {} = {}", kw, ident(&name), t.render(), v.code); self.line(&line); self.bind(&name, t); Ok(()) } /// Expressions that are statements in Rust and statements in Nim too /// (control flow). Returns `None` when it emitted lines itself. fn expr_stmt(&mut self, e: &Expr) -> Result, String> { match e { Expr::If(_) => { self.if_stmt(e)?; Ok(None) } Expr::While(w) => { if w.label.is_some() { return Err("loop labels are not implemented yet".into()); } let c = self.expr(&w.cond)?; self.line(&format!("while {}:", c.code)); let saved = self.target.take(); self.nested_block(&w.body)?; self.target = saved; Ok(None) } Expr::Loop(l) => { if l.label.is_some() { return Err("loop labels are not implemented yet".into()); } self.line("while true:"); let saved = self.target.take(); self.nested_block(&l.body)?; self.target = saved; Ok(None) } Expr::ForLoop(f) => { self.for_loop(f)?; Ok(None) } Expr::Block(b) => { if b.label.is_some() { return Err("block labels are not implemented yet".into()); } self.line("block:"); self.nested_block(&b.block)?; Ok(None) } Expr::Match(_) => { self.match_stmt(e)?; Ok(None) } Expr::Return(r) => { match &r.expr { Some(e) => { let want = self.ret.clone(); let v = self.expr_at(e, want.as_ref())?; self.line(&format!("return {}", v.code)); } None => self.line("return"), } Ok(None) } Expr::Break(b) => { if b.expr.is_some() || b.label.is_some() { return Err("`break` with a value or a label is not implemented yet".into()); } self.line("break"); Ok(None) } Expr::Continue(c) => { if c.label.is_some() { return Err("labelled `continue` is not implemented yet".into()); } self.line("continue"); Ok(None) } Expr::Assign(a) => { let lhs = self.expr(&a.left)?; if !expressible(&a.right) { let target = lhs.code.clone(); return self.assign_from(&a.right, &target, lhs.ty.as_ref()).map(|_| None); } let rhs = self.expr_at(&a.right, lhs.ty.as_ref())?; self.line(&format!("{} = {}", lhs.code, rhs.code)); Ok(None) } Expr::Binary(b) if is_compound(&b.op) => { let lhs = self.expr(&b.left)?; // `i += 1` must widen the literal to `i`'s type, not to the // i32 an unconstrained Rust literal would default to. let rhs = self.expr_at(&b.right, lhs.ty.as_ref())?; let op = self.bin_op(&b.op, &lhs, &rhs)?; // Nim has no `shl=` etc., and `+=` on a `let` is illegal in // both languages, so the expanded form is always correct. self.line(&format!("{} = {} {} {}", lhs.code, lhs.code, op, rhs.code)); Ok(None) } Expr::Macro(m) => { let line = self.macro_call(&m.mac)?; self.line(&line); Ok(None) } _ => Ok(Some(self.expr(e)?)), } } /// Lower `e` in statement position, assigning each arm's value to /// `target`. This is how Rust's expression-oriented `if`/`match` survive /// the trip when their arms are too big for a Nim `if`-expression. fn assign_from( &mut self, e: &Expr, target: &str, expect: Option<&Nim>, ) -> Result<(), String> { let saved = self.target.replace((target.to_string(), expect.cloned())); let r = match e { Expr::If(_) => self.if_stmt(e), Expr::Match(_) => self.match_stmt(e), other => { let v = self.expr_at(other, expect)?; self.line(&format!("{} = {}", target, v.code)); Ok(()) } }; self.target = saved; r } /// Emit a block's value into the active assignment target, if there is /// one, or discard it if there is not. fn emit_tail(&mut self, v: Option) { let Some(v) = v else { return }; match self.target.clone() { Some((t, _)) => { let code = v.code.clone(); self.line(&format!("{t} = {code}")); } None => { let needs = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit); let code = v.code.clone(); if needs { self.line(&format!("discard {code}")); } else if !code.is_empty() { self.line(&code); } } } } fn if_stmt(&mut self, e: &Expr) -> Result<(), String> { let Expr::If(i) = e else { unreachable!() }; if let Expr::Let(_) = &*i.cond { return Err("`if let` is not implemented yet".into()); } let c = self.expr(&i.cond)?; self.line(&format!("if {}:", c.code)); self.nested_block(&i.then_branch)?; match &i.else_branch { None => {} Some((_, els)) => match &**els { Expr::If(_) => { // Nim needs `elif`; splice the nested `if` in as one. let mark = self.out.len(); self.if_stmt(els)?; let tail = self.out.split_off(mark); let indent = " ".repeat(self.indent); self.out.push_str(&tail.replacen(&format!("{indent}if "), &format!("{indent}elif "), 1)); } Expr::Block(b) => { self.line("else:"); self.nested_block(&b.block)?; } _ => return Err("unsupported `else` form".into()), }, } Ok(()) } fn for_loop(&mut self, f: &syn::ExprForLoop) -> Result<(), String> { if f.label.is_some() { return Err("loop labels are not implemented yet".into()); } let name = match &*f.pat { Pat::Ident(i) => i.ident.to_string(), Pat::Wild(_) => "_".into(), _ => return Err("destructuring `for` patterns are not implemented yet".into()), }; // Strip the iterator adaptors that are no-ops once we are iterating a // Nim container directly. Anything else (`.map`, `.filter`, `.rev`) // is a real iterator and is rejected rather than silently dropped. let mut src = &*f.expr; loop { match src { Expr::MethodCall(m) if matches!(m.method.to_string().as_str(), "iter" | "into_iter" | "iter_mut") && m.args.is_empty() => { src = &m.receiver } Expr::Reference(r) => src = &r.expr, _ => break, } } let (header, elem) = match src { Expr::Range(r) => { let lo = match &r.start { Some(e) => self.expr(e)?, None => return Err("a `for` over `..n` needs a start bound".into()), }; let hi = match &r.end { Some(e) => self.expr(e)?, None => return Err("a `for` over an unbounded range would not terminate".into()), }; let op = match r.limits { syn::RangeLimits::HalfOpen(_) => "..<", syn::RangeLimits::Closed(_) => "..", }; let t = lo.ty.clone().or(hi.ty.clone()); (format!("{} {} {}", lo.code, op, hi.code), t) } other => { let v = self.expr(other)?; let elem = match v.ty.clone() { Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) | Some(Nim::Array(_, t)) => Some(*t), Some(Nim::Prim(p)) if p == "string" => Some(Nim::Prim("char".into())), _ => None, }; (v.code, elem) } }; self.line(&format!("for {} in {}:", ident(&name), header)); self.push_scope(); if let Some(t) = elem { self.bind(&name, t); } self.indent += 1; let before = self.out.len(); let saved = self.target.take(); if let Some(v) = self.block_body(&f.body)? { let code = v.code.clone(); self.line(&format!("discard {code}")); } self.target = saved; if self.out.len() == before { self.line("discard"); } self.indent -= 1; self.pop_scope(); Ok(()) } fn match_stmt(&mut self, e: &Expr) -> Result<(), String> { let Expr::Match(m) = e else { unreachable!() }; let scrut = self.expr(&m.expr)?; // A `match` whose arms are all literal or `_` patterns is a Nim `case`, // which is exhaustiveness-checked the same way. Anything richer is // rejected rather than flattened into an if-chain that loses the // check. let name = self.fresh("Match"); let t = scrut .ty .clone() .ok_or("cannot infer the type of a `match` scrutinee")?; self.line(&format!("let {}: {} = {}", name, t.render(), scrut.code)); self.line(&format!("case {}", name)); let mut saw_wild = false; for arm in &m.arms { match &arm.pat { Pat::Guard(_) => { return Err("`match` guards are not implemented yet".into()) } Pat::Wild(_) => { saw_wild = true; self.line("else:"); } p => { let labels = self.pat_labels(p, Some(&t))?; self.line(&format!("of {}:", labels.join(", "))); } } self.indent += 1; let before = self.out.len(); match &*arm.body { Expr::Block(b) => { self.indent -= 1; self.nested_block(&b.block)?; self.indent += 1; } other => { let v = self.expr_stmt(other)?; self.emit_tail(v); } } if self.out.len() == before { self.line("discard"); } self.indent -= 1; } if !saw_wild { // Rust checked exhaustiveness already, but Nim cannot always see // it (an integer `case` needs every value covered), so make the // unreachable arm explicit rather than leaving a compile error. self.line("else:"); self.line(" rsPanic(\"unreachable match arm\")"); } Ok(()) } fn pat_labels(&mut self, p: &Pat, expect: Option<&Nim>) -> Result, String> { match p { Pat::Lit(l) => Ok(vec![self.lit_at(&l.lit, expect)?.code]), Pat::Or(o) => { let mut out = Vec::new(); for p in &o.cases { out.extend(self.pat_labels(p, expect)?); } Ok(out) } Pat::Range(r) => { let lo = r.start.as_ref().ok_or("open-ended range pattern")?; let hi = r.end.as_ref().ok_or("open-ended range pattern")?; let (lo, hi) = (self.expr_at(lo, expect)?, self.expr_at(hi, expect)?); let op = match r.limits { syn::RangeLimits::HalfOpen(_) => "..<", syn::RangeLimits::Closed(_) => "..", }; Ok(vec![format!("{} {} {}", lo.code, op, hi.code)]) } Pat::Path(p) => Ok(vec![ident(&path_name(&p.path))]), _ => Err("unsupported `match` pattern; only literals, ranges, `|` \ alternatives and `_` are implemented" .into()), } } // --------------------------------------------------------- expressions fn expr(&mut self, e: &Expr) -> Result { self.expr_at(e, None) } /// Lower `e`, with the type the surrounding code expects of it. /// /// Rust infers an unsuffixed integer literal's type from its context and /// falls back to `i32`; Nim falls back to 64-bit `int`. Carrying the /// expected type down to the literal is what makes `let x: u8 = 255` and /// `x.wrapping_add(100)` mean the same thing on both sides. Without it the /// widths silently diverge, which is exactly the class of bug this /// project refuses to ship. fn expr_at(&mut self, e: &Expr, expect: Option<&Nim>) -> Result { match e { Expr::Lit(l) => self.lit_at(&l.lit, expect), Expr::Path(p) => { let name = path_name(&p.path); match name.as_str() { "None" => Ok(Val::untyped("rsNone()")), _ => { let t = self.lookup(&name); Ok(Val::new(ident(&name), t)) } } } Expr::Paren(p) => { let v = self.expr_at(&p.expr, expect)?; Ok(Val::new(format!("({})", v.code), v.ty)) } Expr::Group(g) => self.expr_at(&g.expr, expect), // `&x` is a value in Nim; `&mut x` in an argument position binds to // a `var` parameter, which is also just `x` at the call site. Expr::Reference(r) => self.expr_at(&r.expr, expect), Expr::Unary(u) => self.unary(u, expect), Expr::Binary(b) => self.binary(b, expect), Expr::Cast(c) => self.cast(c), Expr::Index(i) => { let base = self.expr(&i.expr)?; let idx = self.expr(&i.index)?; // Rust indexes with usize; Nim wants an `int`, and a `uint` // index is a type error there rather than a silent conversion. let idx_code = match &idx.ty { Some(t) if t.is_unsigned() => format!("int({})", idx.code), _ => idx.code.clone(), }; let elem = match base.ty.clone() { Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) | Some(Nim::Array(_, t)) => Some(*t), Some(Nim::Prim(p)) if p == "string" => Some(Nim::Prim("char".into())), _ => None, }; Ok(Val::new(format!("{}[{}]", base.code, idx_code), elem)) } Expr::Field(f) => { let base = self.expr(&f.base)?; let name = match &f.member { syn::Member::Named(n) => n.to_string(), syn::Member::Unnamed(i) => format!("f{}", i.index), }; let t = match &base.ty { Some(Nim::Named(s, _)) => self .structs .get(s) .and_then(|fs| fs.iter().find(|(f, _)| *f == name)) .map(|(_, t)| t.clone()), _ => None, }; Ok(Val::new(format!("{}.{}", base.code, ident(&name)), t)) } Expr::Call(c) => self.call(c), Expr::MethodCall(m) => self.method(m), Expr::Macro(m) => { let code = self.macro_call(&m.mac)?; Ok(Val::new(code, None)) } Expr::Struct(s) => { let name = path_name(&s.path); let mut parts = Vec::new(); for f in &s.fields { let fname = match &f.member { syn::Member::Named(n) => n.to_string(), syn::Member::Unnamed(i) => format!("f{}", i.index), }; let v = self.expr(&f.expr)?; parts.push(format!("{}: {}", ident(&fname), v.code)); } if s.rest.is_some() { return Err("struct update syntax `..rest` is not implemented yet".into()); } Ok(Val::new( format!("{}({})", ident(&name), parts.join(", ")), Some(Nim::Named(name, vec![])), )) } Expr::Array(a) => { let mut parts = Vec::new(); let mut elem = match expect { Some(Nim::Array(_, t)) | Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) => { Some((**t).clone()) } _ => None, }; for e in &a.elems { let want = elem.clone(); let v = self.expr_at(e, want.as_ref())?; elem = elem.or(v.ty.clone()); parts.push(v.code); } let t = elem.map(|t| Nim::Array(a.elems.len(), Box::new(t))); Ok(Val::new(format!("[{}]", parts.join(", ")), t)) } Expr::Repeat(r) => { let v = self.expr(&r.expr)?; let n = self.expr(&r.len)?; let t = v.ty.clone().map(|t| Nim::Seq(Box::new(t))); Ok(Val::new(format!("newSeqWith(int({}), {})", n.code, v.code), t)) } Expr::Tuple(t) if t.elems.is_empty() => Ok(Val::new("", Some(Nim::Unit))), Expr::Tuple(t) => { let mut parts = Vec::new(); let mut tys = Vec::new(); for e in &t.elems { let v = self.expr(e)?; tys.push(v.ty.clone()); parts.push(v.code); } let ty = tys .iter() .cloned() .collect::>>() .map(Nim::Tuple); Ok(Val::new(format!("({})", parts.join(", ")), ty)) } // `if` and `match` are expressions in both languages, but only // when every arm is itself a single expression. Expr::If(i) => self.if_expr(i, expect), Expr::Block(b) if b.block.stmts.len() == 1 => { if let Some(Stmt::Expr(e, None)) = b.block.stmts.first() { self.expr_at(e, expect) } else { Err("block expression with statements in value position is not implemented yet".into()) } } other => Err(format!( "unsupported expression in value position: {}", expr_kind(other) )), } } fn if_expr(&mut self, i: &syn::ExprIf, expect: Option<&Nim>) -> Result { let (Some(then), Some((_, els))) = (single_expr(&i.then_branch), &i.else_branch) else { return Err( "an `if` used as a value must have an `else` and single-expression arms".into(), ); }; let c = self.expr(&i.cond)?; let t = self.expr_at(then, expect)?; let want = expect.cloned().or_else(|| t.ty.clone()); let e = match &**els { Expr::Block(b) => match single_expr(&b.block) { Some(x) => self.expr_at(x, want.as_ref())?, None => return Err("an `if` used as a value must have single-expression arms".into()), }, other => self.expr_at(other, want.as_ref())?, }; let ty = t.ty.clone().or(e.ty.clone()); Ok(Val::new( format!("(if {}: {} else: {})", c.code, t.code, e.code), ty, )) } fn lit_at(&mut self, l: &Lit, expect: Option<&Nim>) -> Result { match l { Lit::Int(i) => { let suffix = i.suffix(); if let Some(why) = ty::rejected(suffix) { return Err(format!("integer literal `{}`: {}", i, why)); } let digits = i.base10_digits().to_string(); // Rust's default for an unconstrained integer literal is i32. // Nim's is `int` (64-bit). Making the width explicit is what // keeps overflow behaviour the same on both sides. let t = if suffix.is_empty() { match expect { Some(t) if t.is_integer() => t.clone(), // Rust's fallback for an otherwise-unconstrained // integer literal. _ => Nim::Prim("int32".into()), } } else { ty::prim(suffix).ok_or_else(|| format!("unknown literal suffix `{suffix}`"))? }; Ok(Val::new(format!("{}'{}", digits, nim_suffix(&t)?), Some(t))) } Lit::Float(f) => { let t = match f.suffix() { "" => match expect { Some(Nim::Prim(p)) if p == "float32" => Nim::Prim("float32".into()), _ => Nim::Prim("float64".into()), }, "f64" => Nim::Prim("float64".into()), "f32" => Nim::Prim("float32".into()), s => return Err(format!("unknown float suffix `{s}`")), }; let d = f.base10_digits(); let d = if d.contains('.') || d.contains('e') { d.to_string() } else { format!("{d}.0") }; Ok(Val::new(d, Some(t))) } Lit::Bool(b) => Ok(Val::new(b.value.to_string(), Some(Nim::Prim("bool".into())))), Lit::Str(s) => Ok(Val::new( fmt::nim_str(&s.value()), Some(Nim::Prim("string".into())), )), Lit::Char(c) => Ok(Val::new( format!("Rune({})", c.value() as u32), Some(Nim::Prim("Rune".into())), )), Lit::Byte(b) => Ok(Val::new( format!("{}'u8", b.value()), Some(Nim::Prim("uint8".into())), )), Lit::ByteStr(b) => { let bytes: Vec = b.value().iter().map(|x| format!("{x}'u8")).collect(); Ok(Val::new( format!("@[{}]", bytes.join(", ")), Some(Nim::Seq(Box::new(Nim::Prim("uint8".into())))), )) } other => Err(format!("unsupported literal: {other:?}")), } } fn unary(&mut self, u: &syn::ExprUnary, expect: Option<&Nim>) -> Result { // `-128i8` is a literal in Rust, but `-(128'i8)` in Nim would overflow // the positive half of the range before the negation runs. Folding the // sign into the literal keeps `i8::MIN` and friends expressible. if let (UnOp::Neg(_), Expr::Lit(l)) = (&u.op, &*u.expr) { if matches!(l.lit, Lit::Int(_) | Lit::Float(_)) { let v = self.lit_at(&l.lit, expect)?; return Ok(Val::new(format!("-{}", v.code), v.ty)); } } let v = self.expr_at(&u.expr, expect)?; match u.op { UnOp::Neg(_) => Ok(Val::new(format!("(-{})", v.code), v.ty)), // Rust's `!` is logical on bool and bitwise-complement on integers. // Nim spells those `not` and `not` as well, so one mapping covers // both — but only because Nim overloads `not` the same way. UnOp::Not(_) => Ok(Val::new(format!("(not {})", v.code), v.ty)), UnOp::Deref(_) => Ok(v), _ => Err("unsupported unary operator".into()), } } fn binary(&mut self, b: &syn::ExprBinary, expect: Option<&Nim>) -> Result { // A comparison's operands are unrelated to the `bool` it produces, so // the outer expectation is not passed through to them. let down = match b.op { BinOp::Eq(_) | BinOp::Ne(_) | BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_) | BinOp::Ge(_) | BinOp::And(_) | BinOp::Or(_) => None, _ => expect, }; let mut l = self.expr_at(&b.left, down)?; // Rust unifies the two operand types; propagating whichever side is // known to the other reproduces that, and disagreement then surfaces // as a Nim type error rather than as a silent width change. let mut r = self.expr_at(&b.right, l.ty.as_ref().or(down))?; if l.ty.is_none() && r.ty.is_some() { l = self.expr_at(&b.left, r.ty.as_ref())?; } let r = std::mem::replace(&mut r, Val::untyped("")); let op = self.bin_op(&b.op, &l, &r)?; let ty = match b.op { BinOp::Eq(_) | BinOp::Ne(_) | BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_) | BinOp::Ge(_) | BinOp::And(_) | BinOp::Or(_) => Some(Nim::Prim("bool".into())), // Rust's shift takes its result type from the *left* operand, and // the right may be a different width entirely. BinOp::Shl(_) | BinOp::Shr(_) => l.ty.clone(), _ => l.ty.clone().or(r.ty.clone()), }; Ok(Val::new(format!("({} {} {})", l.code, op, r.code), ty)) } fn bin_op(&mut self, op: &BinOp, l: &Val, r: &Val) -> Result<&'static str, String> { Ok(match op { BinOp::Add(_) | BinOp::AddAssign(_) => "+", BinOp::Sub(_) | BinOp::SubAssign(_) => "-", BinOp::Mul(_) | BinOp::MulAssign(_) => "*", BinOp::Div(_) | BinOp::DivAssign(_) => { // Nim spells integer division `div`. Both languages truncate // toward zero, so once the right operator is chosen the // semantics match, including for negative operands. let t = l.ty.clone().or(r.ty.clone()).ok_or( "cannot tell integer from float division here; annotate the operands", )?; if t.is_integer() { "div" } else { "/" } } BinOp::Rem(_) | BinOp::RemAssign(_) => { let t = l.ty.clone().or(r.ty.clone()).ok_or( "cannot tell integer from float remainder here; annotate the operands", )?; if t.is_integer() { "mod" } else { return Err("float `%` is not implemented yet".into()) } } BinOp::And(_) => "and", BinOp::Or(_) => "or", // Nim's `and`/`or`/`xor` are bitwise on integers and logical on // bools, exactly as Rust's `&`/`|`/`^` are. BinOp::BitAnd(_) | BinOp::BitAndAssign(_) => "and", BinOp::BitOr(_) | BinOp::BitOrAssign(_) => "or", BinOp::BitXor(_) | BinOp::BitXorAssign(_) => "xor", // Settled empirically: Nim's `shr` on a signed integer is // arithmetic, matching Rust. See DESIGN.md. BinOp::Shl(_) | BinOp::ShlAssign(_) => "shl", BinOp::Shr(_) | BinOp::ShrAssign(_) => "shr", BinOp::Eq(_) => "==", BinOp::Ne(_) => "!=", BinOp::Lt(_) => "<", BinOp::Le(_) => "<=", BinOp::Gt(_) => ">", BinOp::Ge(_) => ">=", other => return Err(format!("unsupported binary operator {other:?}")), }) } fn cast(&mut self, c: &syn::ExprCast) -> Result { let v = self.expr(&c.expr)?; let to = ty::map(&c.ty)?; let from = v.ty.clone().ok_or_else(|| { format!( "cannot lower `as {}`: the source type is unknown, and `as` \ truncates, so the source width decides the result", to.render() ) })?; let code = match (&from, &to) { (f, t) if f.is_integer() && t.is_integer() => { // Rust's `as` between integers is a pure bit-width truncation // or sign-extension — never a range check. Nim's `T(x)` *does* // range-check and would raise where Rust wraps, so `cast` is // the only faithful spelling. Probed against both compilers. format!("cast[{}]({})", t.render(), v.code) } (f, Nim::Prim(p)) if f.is_integer() && (p == "float64" || p == "float32") => { format!("{}({})", p, v.code) } (Nim::Prim(b), t) if b == "bool" && t.is_integer() => { format!("{}(ord({}))", t.render(), v.code) } (Nim::Prim(r), t) if r == "Rune" && t.is_integer() => { format!("cast[{}](int32({}))", t.render(), v.code) } (f, Nim::Prim(r)) if f.is_integer() && r == "Rune" => { format!("Rune(int32({}))", v.code) } (Nim::Prim(a), Nim::Prim(b)) if a == b => v.code.clone(), (f, t) if matches!(f, Nim::Prim(p) if p.starts_with("float")) && t.is_integer() => { // Rust saturates float->int casts; Nim rounds and range-errors. // Not the same operation, so it is refused rather than mapped. return Err(format!( "`as {}` from a float: Rust saturates, Nim rounds and range-checks; \ no faithful mapping is implemented", t.render() )); } (f, t) => { return Err(format!( "unsupported cast from `{}` to `{}`", f.render(), t.render() )) } }; Ok(Val::new(code, Some(to))) } fn call(&mut self, c: &syn::ExprCall) -> Result { let Expr::Path(p) = &*c.func else { return Err("only calls to named functions are supported".into()); }; let name = path_name(&p.path); let ptys: Vec = self .fns .get(&name) .map(|s| s.params.clone()) .unwrap_or_default(); let mut args = Vec::new(); for (i, a) in c.args.iter().enumerate() { let want = ptys.get(i).cloned(); args.push(self.expr_at(a, want.as_ref())?); } let codes: Vec = args.iter().map(|a| a.code.clone()).collect(); // Constructors from the prelude. // Constructors that live in the prelude rather than in the input file. if let Some(ctor) = match name.as_str() { "Some" => Some("rsSome"), "Ok" => Some("rsOk"), "Err" => Some("rsErr"), _ => None, } { return Ok(Val::new(format!("{}({})", ctor, codes.join(", ")), None)); } // A bare path that names a primitive type is Rust's tuple-struct-like // conversion, e.g. `String::from(..)`; handled by the method path. let ret = self.fns.get(&name).map(|s| s.ret.clone()); if ret.is_none() && !self.structs.contains_key(&name) { return Err(format!( "call to unknown function `{name}`; only functions defined in \ this file and the supported standard-library subset can be lowered" )); } Ok(Val::new( format!("{}({})", ident(&name), codes.join(", ")), ret, )) } fn method(&mut self, m: &syn::ExprMethodCall) -> Result { let recv = self.expr(&m.receiver)?; let name = m.method.to_string(); // `x.wrapping_add(1)` and `x.min(3)` take an argument of the receiver's // own type; `v.push(e)` takes the element type. let arg_want = match (name.as_str(), &recv.ty) { ("push", Some(Nim::Seq(t))) | ("push", Some(Nim::OpenArray(t))) => Some((**t).clone()), (_, t) => t.clone(), }; let mut args = Vec::new(); for a in &m.args { args.push(self.expr_at(a, arg_want.as_ref())?); } let a0 = args.first().map(|a| a.code.clone()); let rt = recv.ty.clone(); let (code, ty) = match name.as_str() { // Rust's `len()` is `usize`; Nim's is `int`. The conversion is // explicit so that a `usize` binding type-checks on the Nim side. "len" => (format!("uint({}.len)", recv.code), Some(Nim::Prim("uint".into()))), "is_empty" => (format!("({}.len == 0)", recv.code), Some(Nim::Prim("bool".into()))), "push" => (format!("{}.add({})", recv.code, a0.unwrap_or_default()), Some(Nim::Unit)), "clone" | "to_vec" | "to_owned" | "as_slice" | "as_ref" | "as_mut" | "iter" | "into_iter" => (recv.code.clone(), rt.clone()), "unwrap" | "expect" => { let inner = match &rt { Some(Nim::Named(n, a)) if (n == "Option" && a.len() == 1) || (n == "Result" && a.len() == 2) => { Some(a[0].clone()) } _ => None, }; (format!("unwrap({})", recv.code), inner) } "is_some" => (format!("{}.has", recv.code), Some(Nim::Prim("bool".into()))), "is_none" => (format!("(not {}.has)", recv.code), Some(Nim::Prim("bool".into()))), "is_ok" => (format!("{}.ok", recv.code), Some(Nim::Prim("bool".into()))), "is_err" => (format!("(not {}.ok)", recv.code), Some(Nim::Prim("bool".into()))), // Settled empirically: Nim's fixed-width *unsigned* arithmetic // wraps silently, matching Rust's `wrapping_*`. For *signed* types // Nim raises OverflowDefect, so the operation is routed through // the unsigned view of the same width, which is what Rust's // wrapping_* is defined to compute. "wrapping_add" | "wrapping_sub" | "wrapping_mul" => { let op = match name.as_str() { "wrapping_add" => "+", "wrapping_sub" => "-", _ => "*", }; let t = rt.clone().ok_or_else(|| { format!("`{name}` needs a known receiver type to pick the wrapping width") })?; if !t.is_integer() { return Err(format!("`{name}` on a non-integer type")); } let arg = a0.ok_or_else(|| format!("`{name}` takes one argument"))?; if t.is_unsigned() { (format!("({} {} {})", recv.code, op, arg), Some(t)) } else { let u = unsigned_peer(&t)?; ( format!( "cast[{}](cast[{}]({}) {} cast[{}]({}))", t.render(), u, recv.code, op, u, arg ), Some(t), ) } } "abs" => (format!("abs({})", recv.code), rt.clone()), "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()), "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()), "to_string" => (format!("rsDisplay({})", recv.code), Some(Nim::Prim("string".into()))), "as_bytes" | "into_bytes" => ( format!("rsBytes({})", recv.code), Some(Nim::Seq(Box::new(Nim::Prim("uint8".into())))), ), _ => { // A method defined in this file via `impl`. Nim's UFCS makes // the call site spelling identical. if let Some(sig) = self.fns.get(&name) { let ret = sig.ret.clone(); let mut all = vec![recv.code.clone()]; all.extend(args.iter().map(|a| a.code.clone())); (format!("{}({})", ident(&name), all.join(", ")), Some(ret)) } else { return Err(format!( "unsupported method `.{name}()`; it is neither defined in \ this file nor part of the standard-library subset that \ has a verified Nim equivalent" )); } } }; Ok(Val::new(code, ty)) } // -------------------------------------------------------------- macros fn macro_call(&mut self, mac: &syn::Macro) -> Result { let name = path_name(&mac.path); match name.as_str() { "println" | "print" | "eprintln" | "eprint" => { let s = self.format_args(mac)?; let nl = name.ends_with("ln"); Ok(match (name.starts_with('e'), nl) { (false, true) => format!("echo {s}"), (false, false) => format!("stdout.write({s})"), (true, true) => format!("stderr.writeLine({s})"), (true, false) => format!("stderr.write({s})"), }) } "format" => self.format_args(mac), "panic" => { let s = self.format_args(mac)?; Ok(format!("rsPanic({s})")) } "assert" => { let e: Expr = mac.parse_body().map_err(|e| format!("assert!: {e}"))?; let v = self.expr(&e)?; Ok(format!( "(if not ({}): rsPanic(\"assertion failed\"))", v.code )) } "vec" => { let body = mac.tokens.to_string(); if body.trim().is_empty() { return Ok("@[]".into()); } let elems: syn::punctuated::Punctuated = mac .parse_body_with(syn::punctuated::Punctuated::parse_terminated) .map_err(|e| format!("vec!: {e}"))?; let mut parts = Vec::new(); for e in &elems { parts.push(self.expr(e)?.code); } Ok(format!("@[{}]", parts.join(", "))) } other => Err(format!( "unsupported macro `{other}!`; a macro whose expansion is not \ known cannot be lowered faithfully" )), } } /// `println!("{} {}", a, b)` -> a Nim string-concatenation expression. fn format_args(&mut self, mac: &syn::Macro) -> Result { let args: syn::punctuated::Punctuated = mac .parse_body_with(syn::punctuated::Punctuated::parse_terminated) .map_err(|e| format!("format arguments: {e}"))?; let mut it = args.iter(); let Some(Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. })) = it.next() else { if args.is_empty() { return Ok("\"\"".into()); } return Err("the first argument must be a literal format string".into()); }; let rest: Vec<&Expr> = it.collect(); let pieces = fmt::parse(&s.value())?; let mut parts: Vec = Vec::new(); let mut next = 0usize; let mut used = vec![false; rest.len()]; for p in &pieces { match p { fmt::Piece::Lit(l) => parts.push(fmt::nim_str(l)), fmt::Piece::Arg { r#ref, spec } => { let v = match r#ref { fmt::Ref::Next => { let e = rest.get(next).ok_or("too few arguments for format string")?; used[next] = true; next += 1; self.expr(e)? } fmt::Ref::Index(i) => { let e = rest.get(*i).ok_or("format index out of range")?; used[*i] = true; self.expr(e)? } fmt::Ref::Named(n) => { let t = self.lookup(n).ok_or_else(|| { format!("`{{{n}}}` captures `{n}`, which is not in scope") })?; Val::new(ident(n), Some(t)) } }; parts.push(fmt::render_arg(&v.code, spec)); } } } // Rust rejects an argument that no `{}` consumes; so do we, rather // than dropping it from the output. if let Some(i) = used.iter().position(|u| !u) { return Err(format!( "argument {} is never used by the format string", i + 1 )); } Ok(if parts.is_empty() { "\"\"".into() } else { parts.join(" & ") }) } } /// Whether an expression has a direct Nim expression form. /// /// Nim's `if` is an expression only when every arm is a single expression, and /// its `case` is never one here. Anything else has to be lowered as statements /// that assign into a target. fn expressible(e: &Expr) -> bool { match e { Expr::If(i) => { let Some(then) = single_expr(&i.then_branch) else { return false }; if !expressible(then) { return false; } match &i.else_branch { None => false, Some((_, els)) => match &**els { Expr::Block(b) => single_expr(&b.block).is_some_and(expressible), other => expressible(other), }, } } Expr::Match(_) | Expr::Block(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => false, _ => true, } } /// The single expression a block consists of, if that is all it is. An `if` /// can only be lowered as a Nim `if`-expression when both arms are this shape. fn single_expr(b: &syn::Block) -> Option<&Expr> { match (b.stmts.len(), b.stmts.first()) { (1, Some(Stmt::Expr(e, None))) => Some(e), _ => None, } } // --------------------------------------------------------------- utilities fn takes_self(sig: &syn::Signature) -> bool { matches!(sig.inputs.first(), Some(FnArg::Receiver(_))) } fn path_name(p: &syn::Path) -> String { p.segments .last() .map(|s| s.ident.to_string()) .unwrap_or_default() } fn is_compound(op: &BinOp) -> bool { matches!( op, BinOp::AddAssign(_) | BinOp::SubAssign(_) | BinOp::MulAssign(_) | BinOp::DivAssign(_) | BinOp::RemAssign(_) | BinOp::BitAndAssign(_) | BinOp::BitOrAssign(_) | BinOp::BitXorAssign(_) | BinOp::ShlAssign(_) | BinOp::ShrAssign(_) ) } /// The Nim literal suffix for an integer type (`5'i32`). fn nim_suffix(t: &Nim) -> Result<&'static str, String> { let Nim::Prim(p) = t else { return Err("not a primitive integer".into()); }; Ok(match p.as_str() { "int8" => "i8", "int16" => "i16", "int32" => "i32", "int64" => "i64", "int" => "i", "uint8" => "u8", "uint16" => "u16", "uint32" => "u32", "uint64" => "u64", "uint" => "u", other => return Err(format!("no Nim literal suffix for `{other}`")), }) } /// The unsigned integer type of the same width, used to spell `wrapping_*`. fn unsigned_peer(t: &Nim) -> Result<&'static str, String> { let Nim::Prim(p) = t else { return Err("not a primitive integer".into()); }; Ok(match p.as_str() { "int8" => "uint8", "int16" => "uint16", "int32" => "uint32", "int64" => "uint64", "int" => "uint", other => return Err(format!("`{other}` has no unsigned peer")), }) } fn item_kind(i: &Item) -> &'static str { match i { Item::Trait(_) => "`trait`", Item::Enum(_) => "`enum`", Item::Type(_) => "`type` alias", Item::Static(_) => "`static`", Item::Macro(_) => "macro definition", Item::Union(_) => "`union`", Item::ExternCrate(_) => "`extern crate`", Item::ForeignMod(_) => "`extern` block", _ => "item", } } fn expr_kind(e: &Expr) -> &'static str { match e { Expr::Closure(_) => "closure", Expr::Async(_) => "`async` block", Expr::Await(_) => "`.await`", Expr::Try(_) => "`?`", Expr::Range(_) => "range", Expr::Match(_) => "`match` (only statement position is implemented)", Expr::Let(_) => "`let` expression", Expr::Unsafe(_) => "`unsafe` block", Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => "loop (has no value in Nim)", _ => "expression", } }