Factor out a discriminant processing library
diff --git a/syntax/discriminant.rs b/syntax/discriminant.rs
new file mode 100644
index 0000000..2a55468
--- /dev/null
+++ b/syntax/discriminant.rs
@@ -0,0 +1,99 @@
+use proc_macro2::{Literal, Span, TokenStream};
+use quote::ToTokens;
+use std::collections::HashSet;
+use std::fmt::{self, Display};
+use std::str::FromStr;
+use syn::{Error, Expr, Lit, Result};
+
+pub struct DiscriminantSet {
+    values: HashSet<Discriminant>,
+    previous: Option<Discriminant>,
+}
+
+#[derive(Copy, Clone, Hash, Eq, PartialEq)]
+pub struct Discriminant {
+    magnitude: u32,
+}
+
+impl DiscriminantSet {
+    pub fn new() -> Self {
+        DiscriminantSet {
+            values: HashSet::new(),
+            previous: None,
+        }
+    }
+
+    pub fn insert(&mut self, expr: &Expr) -> Result<Discriminant> {
+        let discriminant = expr_to_discriminant(expr)?;
+        insert(self, discriminant)
+    }
+
+    pub fn insert_next(&mut self) -> Result<Discriminant> {
+        let discriminant = match self.previous {
+            None => Discriminant::zero(),
+            Some(mut discriminant) => {
+                if discriminant.magnitude == u32::MAX {
+                    let msg = format!("discriminant overflow on value after {}", u32::MAX);
+                    return Err(Error::new(Span::call_site(), msg));
+                }
+                discriminant.magnitude += 1;
+                discriminant
+            }
+        };
+        insert(self, discriminant)
+    }
+}
+
+fn expr_to_discriminant(expr: &Expr) -> Result<Discriminant> {
+    if let Expr::Lit(expr) = expr {
+        if let Lit::Int(lit) = &expr.lit {
+            return lit.base10_parse::<Discriminant>();
+        }
+    }
+    Err(Error::new_spanned(
+        expr,
+        "enums with non-integer literal discriminants are not supported yet",
+    ))
+}
+
+fn insert(set: &mut DiscriminantSet, discriminant: Discriminant) -> Result<Discriminant> {
+    if set.values.insert(discriminant) {
+        set.previous = Some(discriminant);
+        Ok(discriminant)
+    } else {
+        let msg = format!("discriminant value `{}` already exists", discriminant);
+        Err(Error::new(Span::call_site(), msg))
+    }
+}
+
+impl Discriminant {
+    fn zero() -> Self {
+        Discriminant { magnitude: 0 }
+    }
+}
+
+impl Display for Discriminant {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        Display::fmt(&self.magnitude, f)
+    }
+}
+
+impl ToTokens for Discriminant {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        Literal::u32_unsuffixed(self.magnitude).to_tokens(tokens);
+    }
+}
+
+impl FromStr for Discriminant {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        match s.parse::<u32>() {
+            Ok(magnitude) => Ok(Discriminant { magnitude }),
+            Err(_) => Err(Error::new(
+                Span::call_site(),
+                "discriminant value outside of supported range",
+            )),
+        }
+    }
+}
diff --git a/syntax/mod.rs b/syntax/mod.rs
index d21c083..c109229 100644
--- a/syntax/mod.rs
+++ b/syntax/mod.rs
@@ -4,6 +4,7 @@
 mod attrs;
 pub mod check;
 mod derive;
+mod discriminant;
 mod doc;
 pub mod error;
 pub mod ident;
@@ -17,6 +18,7 @@
 mod tokens;
 pub mod types;
 
+use self::discriminant::Discriminant;
 use self::parse::kw;
 use proc_macro2::{Ident, Span};
 use syn::punctuated::Punctuated;
@@ -106,7 +108,7 @@
 
 pub struct Variant {
     pub ident: Ident,
-    pub discriminant: u32,
+    pub discriminant: Discriminant,
 }
 
 pub enum Type {
diff --git a/syntax/parse.rs b/syntax/parse.rs
index f84ff32..eff14c0 100644
--- a/syntax/parse.rs
+++ b/syntax/parse.rs
@@ -1,3 +1,4 @@
+use crate::syntax::discriminant::DiscriminantSet;
 use crate::syntax::report::Errors;
 use crate::syntax::Atom::*;
 use crate::syntax::{
@@ -6,15 +7,12 @@
 };
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
-use std::collections::HashSet;
-use std::u32;
 use syn::parse::{ParseStream, Parser};
 use syn::punctuated::Punctuated;
 use syn::{
-    Abi, Attribute, Error, Expr, ExprLit, Fields, FnArg, ForeignItem, ForeignItemFn,
-    ForeignItemType, GenericArgument, Ident, Item, ItemEnum, ItemForeignMod, ItemStruct, Lit, Pat,
-    PathArguments, Result, ReturnType, Token, Type as RustType, TypeBareFn, TypePath,
-    TypeReference, TypeSlice, Variant as RustVariant,
+    Abi, Attribute, Error, Fields, FnArg, ForeignItem, ForeignItemFn, ForeignItemType,
+    GenericArgument, Ident, Item, ItemEnum, ItemForeignMod, ItemStruct, Pat, PathArguments, Result,
+    ReturnType, Token, Type as RustType, TypeBareFn, TypePath, TypeReference, TypeSlice,
 };
 
 pub mod kw {
@@ -108,8 +106,7 @@
     let doc = attrs::parse_doc(cx, &item.attrs);
 
     let mut variants = Vec::new();
-    let mut discriminants = HashSet::new();
-    let mut prev_discriminant = None;
+    let mut discriminants = DiscriminantSet::new();
     for variant in item.variants {
         match variant.fields {
             Fields::Unit => {}
@@ -120,21 +117,19 @@
                 ));
             }
         }
-        if variant.discriminant.is_none() && prev_discriminant == Some(u32::MAX) {
-            let msg = format!("discriminant overflow on value after {}", u32::MAX);
-            return Err(Error::new_spanned(variant, msg));
-        }
-        let discriminant =
-            parse_discriminant(&variant)?.unwrap_or_else(|| prev_discriminant.map_or(0, |n| n + 1));
-        if !discriminants.insert(discriminant) {
-            let msg = format!("discriminant value `{}` already exists", discriminant);
-            return Err(Error::new_spanned(variant, msg));
-        }
+        let expr = variant.discriminant.as_ref().map(|(_, expr)| expr);
+        let try_discriminant = match &expr {
+            Some(lit) => discriminants.insert(lit),
+            None => discriminants.insert_next(),
+        };
+        let discriminant = match try_discriminant {
+            Ok(discriminant) => discriminant,
+            Err(err) => return Err(Error::new_spanned(variant, err)),
+        };
         variants.push(Variant {
             ident: variant.ident,
             discriminant,
         });
-        prev_discriminant = Some(discriminant);
     }
 
     Ok(Api::Enum(Enum {
@@ -146,28 +141,6 @@
     }))
 }
 
-fn parse_discriminant(variant: &RustVariant) -> Result<Option<u32>> {
-    match &variant.discriminant {
-        None => Ok(None),
-        Some((
-            _,
-            Expr::Lit(ExprLit {
-                lit: Lit::Int(n), ..
-            }),
-        )) => match n.base10_parse() {
-            Ok(val) => Ok(Some(val)),
-            Err(_) => Err(Error::new_spanned(
-                variant,
-                "cannot parse enum discriminant as an integer",
-            )),
-        },
-        _ => Err(Error::new_spanned(
-            variant,
-            "enums with non-integer literal discriminants are not supported yet",
-        )),
-    }
-}
-
 fn parse_foreign_mod(cx: &mut Errors, foreign_mod: ItemForeignMod, out: &mut Vec<Api>) {
     let lang = match parse_lang(foreign_mod.abi) {
         Ok(lang) => lang,