crates/mimir-binary/src/v0.rs
//! A structured Rust **v0** symbol decoder (RFC 2603, the grammar rustc's
//! `rustc_symbol_mangling::v0` writes). `rustc-demangle` only prints text; the
//! binary layer needs the TREE: the crate root's StableCrateId (the `Cs…_`
//! disambiguator), each path segment's namespace (`t` type / `v` value /
//! closures / shims), impl paths with their self type and trait, and the
//! generic args of an instance.
//!
//! Supported: every production rustc emits for ordinary code — crate roots,
//! nested paths, inherent / trait impls, `<T as Trait>` qualification, generic
//! args (types, lifetimes, consts), all basic types, refs, raw pointers,
//! arrays, slices, tuples, fn pointers (abi, unsafe, binders), `dyn` bounds
//! with assoc bindings, backrefs. Const values are decoded to their printed
//! form (ints, bool, char, str, refs, arrays, tuples, ADT consts).
//! NOT supported (decode error, counted by the caller, never guessed):
//! punycode identifiers (`u` prefix), pattern types (`W`), symbols past a
//! nesting depth of 256.
use std::fmt::Write;
/// A decoded path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Path {
/// `C` — a crate root: name + StableCrateId (the v0 disambiguator).
Crate { name: String, id: u64 },
/// `M` — `<SelfTy>` (an inherent impl), `parent` = the impl's parent.
InherentImpl { parent: Box<Path>, self_ty: Box<Type> },
/// `X` — `<SelfTy as Trait>` (a trait impl), `parent` = the impl's parent.
TraitImpl { parent: Box<Path>, self_ty: Box<Type>, trait_: Box<Path> },
/// `Y` — `<SelfTy as Trait>` (an item of the trait definition, used for a
/// provided method instantiated for a Self type).
TraitDef { self_ty: Box<Type>, trait_: Box<Path> },
/// `N` — `parent::name` in namespace `ns` (`t`, `v`, or an uppercase /
/// special one: `C` closure, `S` shim, `K` anon const, `c` ctor …).
Nested { ns: char, parent: Box<Path>, name: String, dis: u64 },
/// `I` — `path::<args>`.
Generic { path: Box<Path>, args: Vec<GenericArg> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GenericArg {
Lifetime,
Type(Type),
Const(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Basic(&'static str),
Path(Path),
Array(Box<Type>, String),
Slice(Box<Type>),
Tuple(Vec<Type>),
Ref { mutable: bool, ty: Box<Type> },
Ptr { mutable: bool, ty: Box<Type> },
Fn { unsafe_: bool, abi: Option<String>, inputs: Vec<Type>, output: Box<Type> },
Dyn(Vec<(Path, Vec<(String, Type)>)>),
}
/// One decoded symbol.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Symbol {
pub path: Path,
/// the crate that instantiated a generic (`<instantiating-crate>`)
pub instantiating_crate: Option<Path>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NotV0,
Invalid(&'static str, usize),
Unsupported(&'static str),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::NotV0 => f.write_str("not a v0 symbol"),
Error::Invalid(w, at) => write!(f, "invalid v0 symbol: {w} at {at}"),
Error::Unsupported(w) => write!(f, "unsupported v0 production: {w}"),
}
}
}
const MAX_DEPTH: u32 = 256;
/// Decode a v0 symbol. Suffixes after the mangled name (`.llvm.123`,
/// `.cold`, `$got` …) are ignored.
pub fn decode(sym: &str) -> Result<Symbol, Error> {
let inner = sym.strip_prefix("_R").ok_or(Error::NotV0)?;
// the mangled name is [A-Za-z0-9_]; anything after is a vendor suffix
let end = inner.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')).unwrap_or(inner.len());
let inner = &inner.as_bytes()[..end];
let mut p = Parser { s: inner, pos: 0, depth: 0 };
// optional encoding version (decimal); only version 0 (absent) exists
if p.peek().is_some_and(|c| c.is_ascii_digit()) {
return Err(Error::Unsupported("encoding version"));
}
let path = p.path()?;
let instantiating_crate = if p.pos < p.s.len() { Some(p.path()?) } else { None };
if p.pos != p.s.len() {
return Err(Error::Invalid("trailing bytes", p.pos));
}
Ok(Symbol { path, instantiating_crate })
}
struct Parser<'a> {
s: &'a [u8],
pos: usize,
depth: u32,
}
impl<'a> Parser<'a> {
fn peek(&self) -> Option<u8> {
self.s.get(self.pos).copied()
}
fn next(&mut self) -> Result<u8, Error> {
let c = self.peek().ok_or(Error::Invalid("unexpected end", self.pos))?;
self.pos += 1;
Ok(c)
}
fn eat(&mut self, c: u8) -> bool {
if self.peek() == Some(c) {
self.pos += 1;
true
} else {
false
}
}
fn enter(&mut self) -> Result<(), Error> {
self.depth += 1;
if self.depth > MAX_DEPTH { Err(Error::Unsupported("nesting depth")) } else { Ok(()) }
}
/// `<base-62-number>`: `_` = 0, else digits `_` = value + 1.
fn integer_62(&mut self) -> Result<u64, Error> {
if self.eat(b'_') {
return Ok(0);
}
let mut x: u64 = 0;
loop {
let c = self.next()?;
if c == b'_' {
break;
}
let d = match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'z' => 10 + (c - b'a'),
b'A'..=b'Z' => 36 + (c - b'A'),
_ => return Err(Error::Invalid("base62 digit", self.pos - 1)),
};
x = x
.checked_mul(62)
.and_then(|x| x.checked_add(d as u64))
.ok_or(Error::Invalid("base62 overflow", self.pos))?;
}
x.checked_add(1).ok_or(Error::Invalid("base62 overflow", self.pos))
}
/// `[s <base-62-number>]` → 0 when absent, else value + 1.
fn disambiguator(&mut self) -> Result<u64, Error> {
if !self.eat(b's') {
return Ok(0);
}
self.integer_62()?.checked_add(1).ok_or(Error::Invalid("disambiguator overflow", self.pos))
}
fn decimal(&mut self) -> Result<usize, Error> {
let start = self.pos;
if self.eat(b'0') {
return Ok(0);
}
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.pos += 1;
}
if self.pos == start {
return Err(Error::Invalid("decimal", start));
}
std::str::from_utf8(&self.s[start..self.pos])
.unwrap()
.parse()
.map_err(|_| Error::Invalid("decimal overflow", start))
}
/// `<undisambiguated-identifier>`
fn ident(&mut self) -> Result<String, Error> {
if self.eat(b'u') {
return Err(Error::Unsupported("punycode identifier"));
}
let len = self.decimal()?;
self.eat(b'_');
let end = self.pos.checked_add(len).filter(|&e| e <= self.s.len()).ok_or(Error::Invalid("identifier length", self.pos))?;
let id = std::str::from_utf8(&self.s[self.pos..end]).map_err(|_| Error::Invalid("identifier utf8", self.pos))?;
self.pos = end;
Ok(id.to_string())
}
/// `B <base-62-number>`: re-parse at an earlier offset.
fn backref<T>(&mut self, f: impl FnOnce(&mut Parser<'a>) -> Result<T, Error>) -> Result<T, Error> {
let at = self.pos - 1;
let target = self.integer_62()? as usize;
if target >= at {
return Err(Error::Invalid("forward backref", at));
}
let mut sub = Parser { s: self.s, pos: target, depth: self.depth };
sub.enter()?;
f(&mut sub)
}
fn path(&mut self) -> Result<Path, Error> {
self.enter()?;
let r = self.path_inner();
self.depth -= 1;
r
}
fn path_inner(&mut self) -> Result<Path, Error> {
let at = self.pos;
Ok(match self.next()? {
b'C' => {
let id = self.disambiguator()?;
let name = self.ident()?;
Path::Crate { name, id }
}
b'M' => {
self.disambiguator()?;
let parent = Box::new(self.path()?);
let self_ty = Box::new(self.ty()?);
Path::InherentImpl { parent, self_ty }
}
b'X' => {
self.disambiguator()?;
let parent = Box::new(self.path()?);
let self_ty = Box::new(self.ty()?);
let trait_ = Box::new(self.path()?);
Path::TraitImpl { parent, self_ty, trait_ }
}
b'Y' => {
let self_ty = Box::new(self.ty()?);
let trait_ = Box::new(self.path()?);
Path::TraitDef { self_ty, trait_ }
}
b'N' => {
let ns = self.next()?;
if !ns.is_ascii_alphabetic() {
return Err(Error::Invalid("namespace", at + 1));
}
let parent = Box::new(self.path()?);
let dis = self.disambiguator()?;
let name = self.ident()?;
Path::Nested { ns: ns as char, parent, name, dis }
}
b'I' => {
let path = Box::new(self.path()?);
let mut args = Vec::new();
while !self.eat(b'E') {
args.push(self.generic_arg()?);
}
Path::Generic { path, args }
}
b'B' => self.backref(|p| p.path())?,
_ => return Err(Error::Invalid("path tag", at)),
})
}
fn generic_arg(&mut self) -> Result<GenericArg, Error> {
if self.eat(b'L') {
self.integer_62()?;
return Ok(GenericArg::Lifetime);
}
if self.eat(b'K') {
return Ok(GenericArg::Const(self.konst()?));
}
Ok(GenericArg::Type(self.ty()?))
}
fn binder(&mut self) -> Result<(), Error> {
if self.eat(b'G') {
self.integer_62()?;
}
Ok(())
}
fn ty(&mut self) -> Result<Type, Error> {
self.enter()?;
let r = self.ty_inner();
self.depth -= 1;
r
}
fn ty_inner(&mut self) -> Result<Type, Error> {
let at = self.pos;
let c = self.next()?;
if let Some(b) = basic(c) {
return Ok(Type::Basic(b));
}
Ok(match c {
b'A' => {
let t = self.ty()?;
let n = self.konst()?;
Type::Array(Box::new(t), n)
}
b'S' => Type::Slice(Box::new(self.ty()?)),
b'T' => {
let mut v = Vec::new();
while !self.eat(b'E') {
v.push(self.ty()?);
}
Type::Tuple(v)
}
b'R' | b'Q' => {
if self.eat(b'L') {
self.integer_62()?;
}
Type::Ref { mutable: c == b'Q', ty: Box::new(self.ty()?) }
}
b'P' | b'O' => Type::Ptr { mutable: c == b'O', ty: Box::new(self.ty()?) },
b'F' => {
self.binder()?;
let unsafe_ = self.eat(b'U');
let abi = if self.eat(b'K') {
if self.eat(b'C') { Some("C".to_string()) } else { Some(self.ident()?.replace('_', "-")) }
} else {
None
};
let mut inputs = Vec::new();
while !self.eat(b'E') {
inputs.push(self.ty()?);
}
let output = Box::new(self.ty()?);
Type::Fn { unsafe_, abi, inputs, output }
}
b'D' => {
self.binder()?;
let mut bounds = Vec::new();
while !self.eat(b'E') {
let path = self.path()?;
let mut assoc = Vec::new();
while self.eat(b'p') {
let name = self.ident()?;
let t = self.ty()?;
assoc.push((name, t));
}
bounds.push((path, assoc));
}
// the object lifetime
if !self.eat(b'L') {
return Err(Error::Invalid("dyn lifetime", self.pos));
}
self.integer_62()?;
Type::Dyn(bounds)
}
b'W' => return Err(Error::Unsupported("pattern type")),
b'B' => self.backref(|p| p.ty())?,
b'C' | b'M' | b'X' | b'Y' | b'N' | b'I' => {
self.pos = at;
Type::Path(self.path()?)
}
_ => return Err(Error::Invalid("type tag", at)),
})
}
/// A const generic arg / array length, decoded to its printed form.
fn konst(&mut self) -> Result<String, Error> {
self.enter()?;
let r = self.konst_inner();
self.depth -= 1;
r
}
fn konst_inner(&mut self) -> Result<String, Error> {
let at = self.pos;
let c = self.next()?;
Ok(match c {
b'p' => "_".to_string(),
b'B' => self.backref(|p| p.konst())?,
b'e' => {
// str: hex bytes then `_`
let hex = self.hex()?;
let bytes: Vec<u8> = (0..hex.len() / 2)
.filter_map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).ok())
.collect();
format!("{:?}", String::from_utf8_lossy(&bytes))
}
b'R' | b'Q' => {
let inner = self.konst()?;
if c == b'Q' { format!("&mut {inner}") } else { format!("&{inner}") }
}
b'A' => {
let mut v = Vec::new();
while !self.eat(b'E') {
v.push(self.konst()?);
}
format!("[{}]", v.join(", "))
}
b'T' => {
let mut v = Vec::new();
while !self.eat(b'E') {
v.push(self.konst()?);
}
if v.len() == 1 { format!("({},)", v[0]) } else { format!("({})", v.join(", ")) }
}
b'V' => {
let path = self.path()?;
let name = print_path(&path);
match self.next()? {
b'U' => name,
b'T' => {
let mut v = Vec::new();
while !self.eat(b'E') {
v.push(self.konst()?);
}
format!("{name}({})", v.join(", "))
}
b'S' => {
let mut v = Vec::new();
while !self.eat(b'E') {
self.disambiguator()?;
let f = self.ident()?;
v.push(format!("{f}: {}", self.konst()?));
}
format!("{name} {{ {} }}", v.join(", "))
}
_ => return Err(Error::Invalid("const variant fields", self.pos - 1)),
}
}
_ => {
let Some(t) = basic(c) else { return Err(Error::Invalid("const type", at)) };
match t {
"bool" => {
let h = self.hex()?;
if h == "0" { "false".into() } else { "true".into() }
}
"char" => {
let h = self.hex()?;
let v = u32::from_str_radix(&h, 16).map_err(|_| Error::Invalid("char const", at))?;
char::from_u32(v).map(|ch| format!("{ch:?}")).unwrap_or_else(|| format!("'\\u{{{h}}}'"))
}
"_" => "_".into(),
_ => {
let neg = self.eat(b'n');
let h = self.hex()?;
let v = u128::from_str_radix(if h.is_empty() { "0" } else { &h }, 16)
.map_err(|_| Error::Invalid("int const", at))?;
if neg { format!("-{v}") } else { v.to_string() }
}
}
}
})
}
fn hex(&mut self) -> Result<String, Error> {
let start = self.pos;
while self.peek().is_some_and(|c| c.is_ascii_hexdigit()) {
self.pos += 1;
}
let h = std::str::from_utf8(&self.s[start..self.pos]).unwrap().to_string();
if !self.eat(b'_') {
return Err(Error::Invalid("hex const terminator", self.pos));
}
Ok(h)
}
}
fn basic(c: u8) -> Option<&'static str> {
Some(match c {
b'a' => "i8",
b'b' => "bool",
b'c' => "char",
b'd' => "f64",
b'e' => "str",
b'f' => "f32",
b'h' => "u8",
b'i' => "isize",
b'j' => "usize",
b'l' => "i32",
b'm' => "u32",
b'n' => "i128",
b'o' => "u128",
b's' => "i16",
b't' => "u16",
b'u' => "()",
b'v' => "...",
b'x' => "i64",
b'y' => "u64",
b'z' => "!",
b'p' => "_",
_ => return None,
})
}
// ------------------------------------------------------------------ printing
/// The hash-free demangled form (`rustc-demangle`'s `{:#}` shape):
/// `a::ident::<u32>`, `<a::Point>::new`, `<b::Dog as a::Greet>::greet`,
/// `a::foo::{closure#0}`. Used as the mono-instance key.
pub fn print_path(p: &Path) -> String {
let mut s = String::new();
write_path(&mut s, p, true);
s
}
fn write_path(s: &mut String, p: &Path, turbofish: bool) {
match p {
Path::Crate { name, .. } => s.push_str(name),
Path::InherentImpl { self_ty, .. } => {
s.push('<');
write_type(s, self_ty);
s.push('>');
}
Path::TraitImpl { self_ty, trait_, .. } | Path::TraitDef { self_ty, trait_ } => {
s.push('<');
write_type(s, self_ty);
s.push_str(" as ");
write_path(s, trait_, false);
s.push('>');
}
Path::Nested { ns, parent, name, dis } => {
write_path(s, parent, turbofish);
// rustc-demangle's rule: special (uppercase) namespaces print as
// `{closure#n}` / `{shim:vtable#n}` / `{K#n}`; internal (lowercase)
// ones print just the name, nothing when it is empty.
if ns.is_ascii_uppercase() {
s.push_str("::{");
match ns {
'C' => s.push_str("closure"),
'S' => s.push_str("shim"),
c => s.push(*c),
}
if !name.is_empty() {
s.push(':');
s.push_str(name);
}
let _ = write!(s, "#{dis}}}");
} else if !name.is_empty() {
s.push_str("::");
s.push_str(name);
}
}
Path::Generic { path, args } => {
write_path(s, path, turbofish);
let args: Vec<&GenericArg> = args.iter().filter(|a| !matches!(a, GenericArg::Lifetime)).collect();
if !args.is_empty() {
if turbofish {
s.push_str("::");
}
s.push('<');
for (i, a) in args.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
match a {
GenericArg::Type(t) => write_type(s, t),
GenericArg::Const(c) => s.push_str(c),
GenericArg::Lifetime => {}
}
}
s.push('>');
}
}
}
}
/// A type, full paths (`alloc::vec::Vec<u8>`).
pub fn print_type(t: &Type) -> String {
let mut s = String::new();
write_type(&mut s, t);
s
}
fn write_type(s: &mut String, t: &Type) {
write_type_with(s, t, &|s, p| write_path(s, p, false));
}
/// A type the way rustc's force-trimmed `Ty` Display prints it — the form the
/// driver writes into impl monikers (`Vec<Point>`, `&str`, `[u8; 4]`).
pub fn print_type_short(t: &Type) -> String {
let mut s = String::new();
write_type_with(&mut s, t, &|s, p| write_path_short(s, p));
s
}
fn write_path_short(s: &mut String, p: &Path) {
// last named segment + its generic args, trimmed recursively
match p {
Path::Generic { path, args } => {
write_path_short(s, path);
let args: Vec<&GenericArg> = args.iter().filter(|a| !matches!(a, GenericArg::Lifetime)).collect();
if !args.is_empty() {
s.push('<');
for (i, a) in args.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
match a {
GenericArg::Type(t) => write_type_with(s, t, &|s, p| write_path_short(s, p)),
GenericArg::Const(c) => s.push_str(c),
GenericArg::Lifetime => {}
}
}
s.push('>');
}
}
Path::Nested { name, .. } => s.push_str(name),
Path::Crate { name, .. } => s.push_str(name),
other => write_path(s, other, false),
}
}
fn write_type_with(s: &mut String, t: &Type, wp: &dyn Fn(&mut String, &Path)) {
match t {
Type::Basic(b) => s.push_str(b),
Type::Path(p) => wp(s, p),
Type::Array(t, n) => {
s.push('[');
write_type_with(s, t, wp);
let _ = write!(s, "; {n}]");
}
Type::Slice(t) => {
s.push('[');
write_type_with(s, t, wp);
s.push(']');
}
Type::Tuple(v) => {
s.push('(');
for (i, t) in v.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
write_type_with(s, t, wp);
}
if v.len() == 1 {
s.push(',');
}
s.push(')');
}
Type::Ref { mutable, ty } => {
s.push_str(if *mutable { "&mut " } else { "&" });
write_type_with(s, ty, wp);
}
Type::Ptr { mutable, ty } => {
s.push_str(if *mutable { "*mut " } else { "*const " });
write_type_with(s, ty, wp);
}
Type::Fn { unsafe_, abi, inputs, output } => {
if *unsafe_ {
s.push_str("unsafe ");
}
if let Some(a) = abi {
let _ = write!(s, "extern \"{a}\" ");
}
s.push_str("fn(");
for (i, t) in inputs.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
write_type_with(s, t, wp);
}
s.push(')');
if !matches!(**output, Type::Basic("()")) {
s.push_str(" -> ");
write_type_with(s, output, wp);
}
}
Type::Dyn(bounds) => {
s.push_str("dyn ");
for (i, (p, assoc)) in bounds.iter().enumerate() {
if i > 0 {
s.push_str(" + ");
}
if assoc.is_empty() {
wp(s, p);
} else {
// `Trait<Assoc = T>`: splice the bindings into the arg list
let mut inner = String::new();
wp(&mut inner, p);
let binds: Vec<String> = assoc
.iter()
.map(|(n, t)| {
let mut x = format!("{n} = ");
write_type_with(&mut x, t, wp);
x
})
.collect();
if let Some(stripped) = inner.strip_suffix('>') {
let _ = write!(s, "{stripped}, {}>", binds.join(", "));
} else {
let _ = write!(s, "{inner}<{}>", binds.join(", "));
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crate_root_disambiguator_is_the_stable_crate_id() {
// from a real 1.98 binary: CU `std.6c98fd8553dbae28-cgu.0`
let s = decode("_RNvNtCs9k3SxhrAWiO_3std2rt10lang_start").unwrap();
let mut p = &s.path;
while let Path::Nested { parent, .. } = p {
p = parent;
}
assert_eq!(p, &Path::Crate { name: "std".into(), id: 0x6c98fd8553dbae28 });
}
#[test]
fn generic_instance_with_instantiating_crate() {
let s = decode("_RINvCsbR8JNGoZHix_1a5identReECsfPIvSHfysBz_1b").unwrap();
assert_eq!(print_path(&s.path), "a::ident::<&str>");
assert!(matches!(s.instantiating_crate, Some(Path::Crate { ref name, .. }) if name == "b"));
let s = decode("_RINvCsbR8JNGoZHix_1a5identmECsfPIvSHfysBz_1b").unwrap();
assert_eq!(print_path(&s.path), "a::ident::<u32>");
}
#[test]
fn trait_impl_and_backrefs() {
let s = decode("_RNvXs1g_NtCsgxBkk5gSRhY_4core3fmtReNtB6_5Debug3fmtCsfPIvSHfysBz_1b").unwrap();
assert_eq!(print_path(&s.path), "<&str as core::fmt::Debug>::fmt");
let s = decode("_RNvMs4_NtCs6i54tJFfzR_5alloc7raw_vecINtB5_6RawVecINtNtB7_5boxed3BoxDINtNtNtCsgxBkk5gSRhY_4core3ops8function5FnMutuEp6OutputINtNtB1b_6result6ResultuNtNtNtB1b_2io5error5ErrorENtNtB1b_6marker4SendNtB2N_4SyncEL_EE8grow_oneCs9k3SxhrAWiO_3std").unwrap();
assert_eq!(
print_path(&s.path),
"<alloc::raw_vec::RawVec<alloc::boxed::Box<dyn core::ops::function::FnMut<(), Output = core::result::Result<(), core::io::error::Error>> + core::marker::Send + core::marker::Sync>>>::grow_one"
);
}
#[test]
fn closures_and_shims_print_like_rustc_demangle() {
for sym in [
"_RNCINvMs0_NtNtCs9k3SxhrAWiO_3std4sync4onceNtB8_4Once15call_once_forceNCINvMNtBa_9once_lockINtB1b_8OnceLockINtNtBa_14reentrant_lock13ReentrantLockINtNtCsgxBkk5gSRhY_4core4cell7RefCellINtNtNtNtBc_2io8buffered10linewriter10LineWriterNtNtB31_5stdio9StdoutRawEEEE10initializeNCINvB1a_11get_or_initNCNvB3I_6stdout0E0zE0E0Bc_",
"_RNvYINtNvNtCs9k3SxhrAWiO_3std9panicking11begin_panic7PayloadReENtNtCsgxBkk5gSRhY_4core5panic12PanicPayload6as_strB9_",
] {
let ours = print_path(&decode(sym).unwrap().path);
let theirs = format!("{:#}", rustc_demangle::demangle(sym));
assert_eq!(ours, theirs, "{sym}");
}
}
}