Centralize mangled symbol joining
diff --git a/syntax/mangle.rs b/syntax/mangle.rs
index a109951..f2616d2 100644
--- a/syntax/mangle.rs
+++ b/syntax/mangle.rs
@@ -1,10 +1,18 @@
 use crate::syntax::namespace::Namespace;
-use crate::syntax::ExternFn;
+use crate::syntax::{symbol, ExternFn};
+
+const CXXBRIDGE: &str = "cxxbridge02";
+
+macro_rules! join {
+    ($($segment:expr),*) => {
+        symbol::join(&[$(&$segment),*])
+    };
+}
 
 pub fn extern_fn(namespace: &Namespace, efn: &ExternFn) -> String {
-    let receiver = match &efn.receiver {
-        Some(receiver) => receiver.ident.to_string() + "$",
-        None => String::new(),
-    };
-    format!("{}cxxbridge02${}{}", namespace, receiver, efn.ident)
+    match &efn.receiver {
+        Some(receiver) => join!(namespace, CXXBRIDGE, receiver.ident, efn.ident),
+        None => join!(namespace, CXXBRIDGE, efn.ident),
+    }
+    .to_string()
 }
diff --git a/syntax/mod.rs b/syntax/mod.rs
index b7ca6b7..81b1a2f 100644
--- a/syntax/mod.rs
+++ b/syntax/mod.rs
@@ -11,6 +11,7 @@
 pub mod namespace;
 mod parse;
 pub mod set;
+mod symbol;
 mod tokens;
 pub mod types;
 
diff --git a/syntax/symbol.rs b/syntax/symbol.rs
new file mode 100644
index 0000000..a40baaf
--- /dev/null
+++ b/syntax/symbol.rs
@@ -0,0 +1,66 @@
+use crate::syntax::namespace::Namespace;
+use proc_macro2::{Ident, TokenStream};
+use quote::ToTokens;
+use std::fmt::{self, Display, Write};
+
+// A mangled symbol consisting of segments separated by '$'.
+// For example: cxxbridge02$string$new
+pub struct Symbol(String);
+
+impl Display for Symbol {
+    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        Display::fmt(&self.0, formatter)
+    }
+}
+
+impl ToTokens for Symbol {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        ToTokens::to_tokens(&self.0, tokens);
+    }
+}
+
+impl Symbol {
+    fn push(&mut self, segment: &dyn Display) {
+        let len_before = self.0.len();
+        if !self.0.is_empty() {
+            self.0.push('$');
+        }
+        self.0.write_fmt(format_args!("{}", segment)).unwrap();
+        assert!(self.0.len() > len_before);
+    }
+}
+
+pub trait Segment: Display {
+    fn write(&self, symbol: &mut Symbol) {
+        symbol.push(&self);
+    }
+}
+
+impl Segment for str {}
+impl Segment for Ident {}
+
+impl Segment for Namespace {
+    fn write(&self, symbol: &mut Symbol) {
+        for segment in self {
+            symbol.push(segment);
+        }
+    }
+}
+
+impl<T> Segment for &'_ T
+where
+    T: ?Sized + Segment,
+{
+    fn write(&self, symbol: &mut Symbol) {
+        (**self).write(symbol);
+    }
+}
+
+pub fn join(segments: &[&dyn Segment]) -> Symbol {
+    let mut symbol = Symbol(String::new());
+    for segment in segments {
+        segment.write(&mut symbol);
+    }
+    assert!(!symbol.0.is_empty());
+    symbol
+}