diff --git a/README.md b/README.md
deleted file mode 100644
--- a/README.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Maxent Phonotactic Learner
-
-A tool for automatically inferring phonotactic grammars from a lexicon and using those grammars to generate random text, based on Hayes and Wilson's [A Maximum Entropy Model of Phonotactics and Phonotactic Learning](http://www.linguistics.ucla.edu/people/hayes/Phonotactics/Index.htm).  This package provides functionality both as a Haskell library and as a command line tool.
-
-To compile this package, run `stack build` in the root of this repository. Run `stack haddock` to build the library documentation. The library may be useful if you wish to use a custom set of candidate constraints beyond the generators offered by the command line tool.
-
-## Command line usage
-
-The command line tool (`phono-learner-hw`) has two commands: `learn`, which infers grammars, and `gensalad`, which generates random text using those grammars. The learn command takes the name of a lexicon file as an argument and outputs a grammar (note this is quite slow). By default the candidates consist of single classes and bigrams, and several; mote constraint types can be added with options. The `gensalad` takes a grammar generated by `learn` and uses it to generate random text. Both commands can also take global options to output their final results to a file, to use a custom-defined feature table for the generation of natural classes, and to control how text is divided into segments.
-
-The command line works as follows:
-
-    phono-learner-hw COMMAND [-t|--featuretable CSVFILE] ([-c|--charsegs] | [-w|--wordsegs] | [--fierrosegs]) [-n|--samples ARG] [-o|--output OUTFILE]
-
-
-| Option | Description |
-| --- | --- |
-| -t, --featuretable *CSVFILE* | Use the features and segment list from a feature table in CSV format (a table for IPA is used by default). |
-| -c, --charsegs             | Use characters as segments (default). |
-| -w, --wordsegs             | Separate segments by spaces. |
-| --fierosegs              | Parse segments by repeatedly taking the longest possible match and use ' to break up unintended digraphs (used for Fiero orthography). |
-| -n, --samples *N*          | Number of samples to use for salad generation. |
-| -o, --output *OUTFILE*       | Record final output to OUTFILE as well as stdout. |
-
-    hw-learner learn LEXICON [--thresholds THRESHOLDS] [-f|--freqs] [-e|--edges] [-3|--trigrams COREFEATURES] [-l|--longdistance SKIPFEATURES] [GLOBALOPTIONS]
-
-| Option | Description |
-| --- | --- |
-| --thresholds *THRESHOLDS* | thresholds to use for candidate selection (default is `[0.01, 0.1, 0.2, 0.3]``). |
-| -f,--freqs              | Lexicon file contains word frequencies.
-| -e,--edges              | Allow constraints involving word boundaries.
-| -3,--trigrams *COREFEATURES* | Allow trigram constraints where at least one class uses a single one of the following features (space separated in quotes). |
-| -l,--longdistance SKIPFEATURES  |Allow constraints with two classes separated by a run of characters possibly restricted to all having one of the following features.
-
-    hw-learner gensalad GRAMMAR [GLOBALOPTIONS]
-
-### Example usage
-
-The following two command calculates a grammar using Hayes and Wilson's Shona test data using their selection of trigram restrictions and then generate random text using it.
-
-
-
-    phono-learner-hw learn ShonaLearningData.txt -f -e -3 "syllabic consonantal sonorant" -t ShonaFeatures.csv -w -o shonalongdistance.txt
-    phono-learner-hw gensalad ShonaGrammar.txt -t ShonaFeatures.csv -w -o ShonaSalad.txt
-
-
-
-## Feature Table Format
-
-To use a feature table other than the default IPA one, you may define it in CSV format (RFC 4180). The segment names are defined by the first row (they may be any strings as long as they are all distinct, i.e. no duplicate names) and the feature names are defined by the first column (they are not hard-coded). Data cells should contain `+`, `-`, or `0` for binary features and `+` or `0` for privative features (where we do not want a minus set that could form classes).
-
-As a simple example, consider the following CSV file, defining three segments (a, n, and t), and two features (vowel and nasal).
-
-         ,a,n,t
-    vowel,+,-,-
-    nasal,0,+,-
-
-If a row contains a different number of cells (separated by commas) than the header line, is rejected as invalid and does not define a feature (and will not be dispayed in the formatted feature table). If the CSV which is entered has duplicate segment names, no segments, or no valid features, the entire table is rejected (indicated by a red border around the text area, green is normal) and the last valid table is used and displayed.
-
----
-
-Copyright © 2016-2017 George Steel and Peter Jurgec.
-
-This project is supported by the University of Toronto Advancing Teaching and Learning in Arts and Science (ATLAS) grant to Peter Jurgec.
-
-This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE ParallelListComp, TemplateHaskell, ScopedTypeVariables #-}
+{-# LANGUAGE ParallelListComp, TemplateHaskell, ScopedTypeVariables, DoAndIfThenElse #-}
 
 {-
 Command line interface for phonotactic learner
@@ -17,6 +17,7 @@
 
 import Text.PhonotacticLearner
 import Text.PhonotacticLearner.PhonotacticConstraints
+import Text.PhonotacticLearner.PhonotacticConstraints.FileFormats
 import Text.PhonotacticLearner.PhonotacticConstraints.Generators
 import Text.PhonotacticLearner.DFST
 import Text.PhonotacticLearner.Util.Ring
@@ -29,12 +30,18 @@
 import Control.Monad.State
 import Control.Applicative
 import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import qualified Data.Text.Encoding as T
+import qualified Data.Text.Encoding.Error as T
+import qualified Data.ByteString as B
 import qualified Data.Map.Lazy as M
+import qualified Data.Set as S
 import Data.Monoid
 import Data.Array.IArray
 import Data.Maybe
 import Data.FileEmbed
 import Data.Char
+import Data.Foldable
 import Text.Read
 import Numeric
 import Control.Arrow
@@ -43,24 +50,27 @@
 import Control.Exception
 import Control.Parallel.Strategies
 import System.Random
+import System.IO
+import System.Random.Shuffle
 
-data SegmentType = Chars | Words | Fiero deriving (Enum, Eq, Ord, Read, Show)
+data SaladOutput = SpacedLex | FieroLex | FieroShuffled deriving (Eq, Ord, Read, Show, Enum)
 
-data Command = Learn {
-        lexicon :: FilePath,
-        thresholds :: [Double],
-        hasFreqs :: Bool,
-        useEdges :: Bool,
-        useTrigrams :: Maybe String,
-        useBroken :: Maybe String }
-    | GenSalad {
-        grammarfile :: FilePath }
+data Command = Learn
+        FilePath -- Lexicon file
+        Bool -- Collate lexicon
+        [Double] -- Thresholds for learner
+        Bool -- Use edges
+        (Maybe String) -- Trigram cores
+        (Maybe String) -- Long distance centers
+    | GenSalad
+        FilePath -- Grammar file
+        Bool -- space outputs
+        Bool  -- shuffle outputs
     deriving Show
 
 data ParsedArgs = ParsedArgs {
     cmd :: Command,
     ftable :: Maybe FilePath,
-    segtype :: SegmentType,
     samplesize :: Int,
     outfile :: Maybe FilePath
 } deriving (Show)
@@ -70,63 +80,32 @@
 parseOpts = ParsedArgs <$>
     hsubparser (command "learn" (info (Learn
             <$> strArgument (metavar "LEXICON")
+            <*> switch (long "collate" <> short 'c' <> help "Collate a lexicon from raw text, spaces separate words. When this is off, one ")
             <*> option auto (long "thresholds" <> metavar "THRESHOLDS" <> value [0.01, 0.1, 0.2, 0.3] <> help "thresholds to use for candidate selection (default is [0.01, 0.1, 0.2, 0.3]).")
-            <*> switch (long "freqs" <> short 'f' <> help "Lexicon file contains word frequencies.")
-            <*> switch (long "edges" <> short 'e' <> help "Allow constraints involving word boundaries.")
+            <*> switch (long "edges" <> short 'e' <> help "Allow single classes and bigrams restricted to word boundaries.")
             <*> optional (strOption $ long "trigrams" <> short '3' <> metavar "COREFEATURES" <>
-                help "Allow trigram constraints where at least one class uses a single one of the following features (comma-separated).")
+                help "Allows trigrams as long as at least one class is [] or [±x] where x is in COREFEATURES (space separated in quotes).")
             <*> optional (strOption $ long "longdistance" <> short 'l' <> metavar "SKIPFEATURES" <>
-                help "Allow constraints with two classes separated by a run of characters possibly restricted to all having one of the following features.")
+                help "Allows long-distance constraints of the form AB+C where A,C are classes and C = [] or [±x] with x in SKIPFEATURES.")
             ) (fullDesc <> progDesc "Learn a phonotactic grammar from a given lexicon"))
-        <> command "gensalad" (info (GenSalad <$> strArgument (metavar "GRAMMAR"))
-            (fullDesc <> progDesc "Generate random words from an already-calculated grammar")))
-    <*> optional (option str $ long "featuretable" <> short 't' <> metavar "CSVFILE" <>
+        <> command "gibber" (info (GenSalad
+            <$> strArgument (metavar "GRAMMAR")
+            <*> switch (long "spaced" <> help "Separate segments with spaces")
+            <*> switch (long "shuffle" <> short 's' <> help "Shuffle generate output (sorted by default)")
+            ) (fullDesc <> progDesc "Generate a salad of random words from an already-calculated grammar")))
+    <*> optional (option str $ long "featuretable" <> short 'f' <> metavar "CSVFILE" <>
         help "Use the features and segment list from a feature table in CSV format (a table for IPA is used by default).")
-    <*> (flag' Chars (long "charsegs" <> short 'c' <> help "Use characters as segments (default).")
-        <|> flag' Words (long "wordsegs" <> short 'w' <> help "Separate segments by spaces.")
-        <|> flag' Fiero (long "fierosegs" <> help "Parse segments by repeatedly taking the longest possible match and use ' to break up unintended digraphs (used for Fiero orthography).")
-        <|> pure Chars)
     <*> option auto (long "samples" <> short 'n' <> value 3000 <> help "Number of samples to use for salad generation.")
     <*> optional (strOption $ long "output" <> short 'o' <> metavar "OUTFILE" <> help "Record final output to OUTFILE as well as stdout.")
 
 opts = info (helper <*> parseOpts) (fullDesc <> progDesc "Automatically infer phonotactic grammars from text and apply them as probability distributions.")
 
 
-
-
 ipaft :: FeatureTable String
 ipaft = fromJust (csvToFeatureTable id $(embedStringFile "./app/ft-ipa.csv"))
 
-freqreader :: FeatureTable String -> (String -> [String]) -> String -> [([SegRef],Int)]
-freqreader ft seg text = do
-    line <- lines text
-    let (wt@(_:_),wf') = break (== '\t') line
-    [wf] <- return (words wf')
-    Just n <- return $ readMaybe wf
-    return (segsToRefs ft (seg wt), n)
-
-nofreqreader :: FeatureTable String -> (String -> [String]) -> String -> [([SegRef],Int)]
-nofreqreader ft seg text = do
-    line <- lines text
-    return (segsToRefs ft (seg line), 1)
-
-prettyprintGrammar :: (Show clabel) => [clabel] -> Vec -> String
-prettyprintGrammar grammar weights = (unlines . reverse) [showFFloat (Just 2) w "  " ++ show c | c <- grammar | w <- coords weights]
-
-isNonComment :: String -> Bool
-isNonComment [] = False
-isNonComment "\n" = False
-isNonComment ('#':_) = False
-isNonComment _ = True
-
-restrictedClasses :: FeatureTable String -> String -> [(NaturalClass, SegSet SegRef)]
-restrictedClasses ft arg = fmap ((id &&& classToSeglist ft) . NClass False) $ [] : do
-    feat <- fmap T.pack (words arg)
-    Just _ <- return $ M.lookup feat (featLookup ft)
-    [[(FPlus, feat)], [(FMinus, feat)]]
-
-
 main = do
+    hSetEncoding stdout utf8
     args <- execParser opts
     putStrLn (show args)
     ft <- case ftable args of
@@ -140,71 +119,62 @@
             return ipaft
 
     case cmd args of
-        Learn lexfile thresh lfreqs gedges gtris gbroken -> do
-            let segmenter = case segtype args of
-                    Words -> words
-                    Chars -> fmap return
-                    Fiero -> segmentFiero (elems (segNames ft))
-                cls = force $ classesByGenerality ft 3
-            lexdata <- readFile lexfile
-            let lexlist = (if lfreqs then freqreader else nofreqreader) ft segmenter lexdata
-            when (null lexlist) (die "Invalid lexicon file")
-            let wfs = sortLexicon lexlist
-                singles = ugSingleClasses cls
-                edges = if gedges then (ugEdgeClasses cls) else []
-                doubles = ugBigrams cls
-                edoubles = if gedges then (ugEdgeBigrams cls) else []
-                triples = case gtris of Just rcls -> ugLimitedTrigrams cls (restrictedClasses ft rcls)
-                                        Nothing -> []
-                longdistance = case gbroken of Just rcls -> ugLongDistance cls (restrictedClasses ft rcls)
-                                               Nothing -> []
+        Learn lexfile docollate thresh edges tris broken -> do
+            rawlex <- fmap (T.decodeUtf8With T.lenientDecode) (B.readFile lexfile)
+            let segs = S.fromList . elems . segNames $ ft
+                parsedLex = (if docollate then collateWordlist else parseWordlist) segs rawlex
+                candsettings = CandidateSettings edges (fmap (T.words . T.pack) tris) (fmap (T.words . T.pack) broken)
+                cookedlex = fmap (\(LexRow w f) -> (segsToRefs ft w, f)) parsedLex
+            when (null cookedlex) (die "Invalid lexicon file")
+            (ncls, nglobs, globs) <- evaluate $ candidateGrammar ft candsettings
+            putStrLn $ "Generated candidates with " ++ show ncls ++ " classes and " ++ show nglobs ++ " globs, running DFA generation in parallel."
 
-            globs <- evaluate . force $ join [singles,edges,doubles,edoubles,triples,longdistance]
-            putStrLn $ "Generated candidates with " ++ show (length cls) ++ " classes and " ++ show (length globs) ++ " globs, running DFA generation in parallel."
             let candidates = fmap (force . (id *** matchCounter)) globs `using` (parListChunk 1000 rdeepseq)
-
-            (grammar, dfa, weights) <- generateGrammarIO (samplesize args) thresh candidates lexlist
-
-            let output = "# Length Distribution:\n" ++ (show . assocs . lengthFreqs $ wfs) ++ "\n\n# Rules:\n" ++ prettyprintGrammar grammar weights
+            (lenarr, grammar, dfa, weights) <- generateGrammarIO (samplesize args) thresh candidates cookedlex
 
+            let output = serGrammar (PhonoGrammar lenarr grammar weights)
             putStrLn "\n\n\n\n"
-            putStrLn output
+            T.putStrLn output
 
             case outfile args of
-                Just outf -> writeFile outf output
+                Just outf -> B.writeFile outf (T.encodeUtf8 output)
                 Nothing -> return ()
 
 
 
-        GenSalad gfile -> do
-            rawgrammar <- readFile gfile
-
-            (fline:glines) <- evaluate $ filter isNonComment (lines rawgrammar)
-
-            let lendist :: [(Int,Int)] = read fline
-                grammar :: [(Double,ClassGlob)] = fmap ((read *** read) . break isSpace) glines
-                lencdf = massToCdf (fmap (second fromIntegral) lendist)
-                (weightlist,rulelist) = unzip (reverse grammar)
-                weights = vec weightlist
-                blankdfa = nildfa (srBounds ft)
-                dfa = foldr (\g t -> force $ dfaProduct consMC (unpackDFA . cgMatchCounter ft $ g) (force t)) blankdfa rulelist
-                unsegmenter = case segtype args of
-                    Words -> unwords
-                    Chars -> join
-                    Fiero -> joinFiero (elems (segNames ft))
+        GenSalad gfile dospace doshuffle -> do
+            rawgrammar <- fmap (T.decodeUtf8With T.lenientDecode) (B.readFile gfile)
+            Just (PhonoGrammar lendist rules ws) <- evaluate . force $ parseGrammar rawgrammar
 
-            evaluate . force $ grammar
-            evaluate . force $ dfa
+            let nrules = length rules
+                lencdf = massToCdf (fmap (second fromIntegral) (assocs lendist))
+                blankdfa :: MulticountDFST SegRef
+                blankdfa = pruneAndPack . nildfa $ srBounds ft
+                addRule :: ClassGlob -> MulticountDFST SegRef -> IO (MulticountDFST SegRef)
+                addRule r g = do
+                    g' <- evaluate . pruneAndPack $ rawIntersection consMC (unpackDFA . cgMatchCounter ft $ r) (unpackDFA g)
+                    hPutStr stderr "#" >> hFlush stderr
+                    return g'
 
-            salad <- getStdRandom . runState $ sampleWordSalad (fmap (maxentProb weights) dfa) lencdf (samplesize args)
+            dfa <- foldrM addRule blankdfa rules
+            hPutStrLn stderr "\n" >> hFlush stderr
+            salad <- getStdRandom . runState $ sampleWordSalad (fmap (maxentProb ws) (unpackDFA dfa)) lencdf (samplesize args)
 
-            let output = unlines . fmap (unsegmenter . refsToSegs ft) $ salad
+            let segs = S.fromList . elems . segNames $ ft
+            output <- if doshuffle then do
+                salad' <- fmap (shuffle' salad (length salad)) newStdGen
+                let saladsegs = fmap (refsToSegs ft) salad'
+                    unseg = if dospace then unwords else joinFiero segs
+                return . T.unlines . fmap (T.pack . unseg) $ saladsegs
+            else return $ let
+                sortedsalad = wordFreqs . sortLexicon . fmap (\x -> (x,1)) $ salad
+                in (if dospace then serWordlistSpaced else serWordlist segs) . fmap (\(w,f) -> LexRow (refsToSegs ft w) f) $ sortedsalad
 
             putStrLn "\n\n\n\n"
-            putStrLn output
+            T.putStrLn output
 
             case outfile args of
-                Just outf -> writeFile outf output
+                Just outf -> B.writeFile outf (T.encodeUtf8 output)
                 Nothing -> return ()
 
             return ()
diff --git a/app/ft-ipa.csv b/app/ft-ipa.csv
--- a/app/ft-ipa.csv
+++ b/app/ft-ipa.csv
@@ -1,25 +1,17 @@
-            ,t,d,s,z,ɬ,ɮ,θ,ð,ʃ,ʒ,c,ɟ,ç,ʝ,ʈ,ɖ,ʂ,ʐ,p,b,f,v,ɸ,β,k,g,x,ɣ,q,ɢ,χ,ʁ,ħ,ʕ,h,ɦ,ʔ,ʧ,ʤ,ʦ,ʣ,m,ɱ,n,ŋ,ɳ,ɲ,ɴ,l,ɭ,ʎ,ʟ,ⱱ,ɾ,ɽ,ʙ,r,ʀ,ʋ,ɹ,ɻ,j,w,ɥ,ɰ,i,y,ɪ,ʏ,ɨ,ʉ,ɯ,u,ʊ,e,ø,ɘ,ɵ,ɤ,o,ə,ɛ,œ,ɜ,ɞ,ʌ,ɔ,æ,ɐ,a,ɶ,ɑ,ɒ
-consonantal, +,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,-,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-sonorant,    -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
-syllabic,    -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
-labial,      -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,-,-,-,-,-,-,-,-,-,+,-,-,+,-,-,+,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
-round,       0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,-,-,-,-,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,+,+,-,-,+,-,+,-,+,+,+,+,-,+,-,+,-,+,-,-,+,-,+,-,+,-,-,-,+,-,+
-coronal,     +,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,+,-,+,-,-,+,+,-,-,-,+,+,-,+,-,-,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-anterior,    +,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,-,+,+,0,0,+,0,-,0,0,+,-,0,0,0,+,-,0,+,0,0,+,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
-distributed, -,-,-,-,-,-,-,-,+,+,+,+,+,+,-,-,-,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,+,+,-,-,0,0,-,0,-,0,0,-,-,0,0,0,-,-,0,-,0,0,-,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
-dorsal,      -,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,+,+,-,-,-,-,-,+,+,-,-,-,-,-,+,+,+,+,-,-,+,+,-,-,-,-,-,+,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
-high,        0,0,0,0,0,0,0,0,0,0,+,+,+,+,0,0,0,0,0,0,0,0,0,0,+,+,+,+,-,-,-,-,0,0,0,0,0,+,+,0,0,0,0,0,+,-,+,-,0,0,+,+,0,0,0,0,0,-,0,0,0,+,+,+,+,+,+,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-low,         0,0,0,0,0,0,0,0,0,0,-,-,-,-,0,0,0,0,0,0,0,0,0,0,-,-,-,-,-,-,-,-,0,0,0,0,0,-,-,0,0,0,0,0,-,-,-,-,0,0,-,-,0,0,0,0,0,-,0,0,0,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+
-back,        0,0,0,0,0,0,0,0,0,0,-,-,-,-,0,0,0,0,0,0,0,0,0,0,+,+,+,+,+,+,+,+,0,0,0,0,0,-,-,0,0,0,0,0,+,-,-,+,0,0,-,+,0,0,0,0,0,+,0,0,0,-,+,-,+,-,-,-,-,-,-,+,+,+,-,-,-,-,+,+,-,-,-,-,-,+,+,-,-,-,-,+,+
-front,       +,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,+,-,+,+,-,+,+,+,+,-,-,-,-,-,-,-,-,-,+,-,+,-,+,+,+,+,-,-,-,-,-,+,+,-,-,-,-,-,+,+,-,-,-,-,+,-,+,+,-,-
-tense,       0,0,0,0,0,0,0,0,0,0,-,-,-,-,0,0,0,0,0,0,0,0,0,0,-,-,-,-,-,-,-,-,0,0,0,0,0,-,-,0,0,0,0,0,-,-,-,-,0,0,-,-,0,0,0,0,0,-,0,0,0,-,-,-,-,+,+,-,-,+,+,+,+,-,+,+,+,+,+,+,-,-,-,-,-,-,-,+,+,-,+,-,-
-pharyngeal,  -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
-ATR,         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,+,+,-,-,+,+,-,+,-,+,+,+,+,+,+,0,-,-,-,-,-,-,-,+,-,-,-,-
-voice,       -,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,-,+,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
-s.g.,        -,-,-,-,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-c.g.,        -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-continuant,  -,-,+,+,+,+,+,+,+,+,-,-,+,+,-,-,+,+,-,-,+,+,+,+,-,-,+,+,-,-,+,+,+,+,+,+,-,0,0,0,0,-,-,-,-,-,-,-,+,+,+,+,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
-strident,    -,-,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,-,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-lateral,     -,-,-,-,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-del.rel.,    -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
-nasal,       -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
+            ,t,d,s,z,ɬ,ɮ,ʃ,ʒ,c,ɟ,p,b,f,v,k,ɡ,x,ɣ,h,ɦ,ʔ,m,n,ŋ,ɲ,l,ʎ,ʟ,ⱱ,ɽ,r,ʀ,ʋ,j,w,i,y,ɪ,ʏ,ɯ,u,ʊ,e,ø,ɤ,o,ə,ɛ,œ,ʌ,ɔ,æ,ɐ,a,ɶ,ɑ,ɒ
+syllabic,    -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
+sonorant,    -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
+consonantal, +,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,-,-,-,+,+,+,+,+,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
+continuant,  -,-,+,+,+,+,+,+,-,-,-,-,+,+,-,-,+,+,+,+,-,-,-,-,-,+,+,+,-,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
+nasal,       -,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
+lateral,     -,-,-,-,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
+labial,      -,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,-,-,-,-,-,+,-,-,-,-,-,-,+,-,-,-,+,+,+,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+coronal,     +,+,+,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,+,-,+,+,+,-,-,+,+,-,-,-,-,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+anterior,    +,+,+,+,+,+,-,-,-,-,0,0,0,0,0,0,0,0,0,0,0,0,+,0,-,+,-,0,0,-,+,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+dorsal,      -,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,-,-,-,-,-,+,-,-,-,+,-,-,-,+,-,-,+,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+round,       0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,+,-,+,+,+,+,-,+,-,+,-,-,+,-,+,-,-,-,+,-,+
+back,        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,-,-,-,+,+,+,-,-,+,+,-,-,-,+,+,-,-,-,-,+,+
+high,        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,+,+,+,+,+,+,+,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-
+low,         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,+,+,+,+,+,+
+ATR,         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,+,+,-,-,-,+,-,+,+,+,+,0,-,-,-,-,-,-,-,-,-,-
+voice,       -,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,-,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+
diff --git a/maxent-learner-hw.cabal b/maxent-learner-hw.cabal
--- a/maxent-learner-hw.cabal
+++ b/maxent-learner-hw.cabal
@@ -1,5 +1,5 @@
 name:                maxent-learner-hw
-version:             0.1.2
+version:             0.2.0
 synopsis:            Hayes and Wilson's maxent learning algorithm for phonotactic grammars.
 description:         Provides an implementation of Hayes and Wilson's machine learning algorithm for maxent phonotactic grammars, as both a command-line tool and a function library.  The learner takes in a lexicon and produces a list of weighted constraints penalizing certain sound sequemces in an attempt to produce a probability distribution of words which maximizes the probability of the lexicon. Once such a set of constraints is generated, it can be tested by using it to generate random pronounceable text.
                      .
@@ -9,10 +9,10 @@
 license-file:        LICENSE
 author:              George Steel
 maintainer:          george.steel@gmail.com
-copyright:           2016 George Steel and Peter Jurgec
+copyright:           2016-2017 George Steel and Peter Jurgec
 category:            Linguistics
 build-type:          Simple
-extra-source-files:  README.md, ffisrc/packeddfa.c, app/ft-ipa.csv
+extra-source-files:  ffisrc/packeddfa.c, app/ft-ipa.csv
 cabal-version:       >=1.10
 
 library
@@ -24,6 +24,7 @@
                      , Text.PhonotacticLearner.DFST
                      , Text.PhonotacticLearner.MaxentGrammar
                      , Text.PhonotacticLearner.PhonotacticConstraints
+                     , Text.PhonotacticLearner.PhonotacticConstraints.FileFormats
                      , Text.PhonotacticLearner.PhonotacticConstraints.Generators
                      , Text.PhonotacticLearner
   build-depends:       base >= 4.7 && < 5
@@ -31,10 +32,11 @@
                      , vector >= 0.10
                      , mtl >= 2.1 && < 2.3
                      , containers == 0.5.*
+                     , parallel == 3.2.*
                      , random == 1.1
                      , array >= 0.3 && < 0.6
                      , text == 1.2.*
-                     , csv == 0.1.*
+                     , readcsv
   default-language:    Haskell2010
   c-sources:           ffisrc/packeddfa.c
 
@@ -53,8 +55,10 @@
                      , array >= 0.3 && < 0.6
                      , mtl >= 2.1 && < 2.3
                      , random == 1.1
+                     , bytestring
+                     , random-shuffle
   default-language:    Haskell2010
 
 source-repository head
   type:     git
-  location: https://github.com/githubuser/maxent-learner
+  location: https://github.com/george-steel/maxent-learner
diff --git a/src/Text/PhonotacticLearner.hs b/src/Text/PhonotacticLearner.hs
--- a/src/Text/PhonotacticLearner.hs
+++ b/src/Text/PhonotacticLearner.hs
@@ -11,9 +11,7 @@
 -}
 
 module Text.PhonotacticLearner(
-    generateGrammarIO,
-
-    segmentFiero, joinFiero,
+    generateGrammarIO, generateGrammarCB
 ) where
 
 import Text.PhonotacticLearner.Util.Ring
@@ -28,6 +26,8 @@
 import Numeric
 import Data.IORef
 import Data.List
+import Data.Foldable
+import Data.Array.IArray
 import System.IO
 import Control.Exception
 
@@ -55,85 +55,93 @@
     -> [Double] -- ^ List of accuracy thresholds
     -> [(clabel, ShortDFST sigma)] -- ^ List of candidate constraints. Labels must be unique. All DFSTs must share the same input bounds.
     -> [([sigma],Int)] -- ^ Corpus of sample words and their relative frequencies
-    -> IO ([clabel], MulticountDFST sigma, Vec) -- ^ Computed grammar
+    -> IO (Array Length Int, [clabel], MulticountDFST sigma, Vec) -- ^ Computed grammar
+
 generateGrammarIO samplesize thresholds candidates wfs = do
+    let cbound = psegBounds . snd . head $ candidates
+        blankdfa = pruneAndPack (nildfa cbound)
+    grammarref <- newIORef (accumArray (+) 0 (0,0) [], [], blankdfa, zero)
+    let progresscb _ _ = return ()
+        grammarcb lenarr rules dfa ws = mask_ $ atomicWriteIORef grammarref (lenarr,rules,dfa,ws)
+    void (generateGrammarCB progresscb grammarcb samplesize thresholds candidates wfs) `catch` stopsigint
+    readIORef grammarref
+
+{-|
+Like 'generateGrammarIO' but calls callbacks to report progress and non-final grammars. Useful to run in the background and display intermediate results
+Does not catch SIGINT.
+-}
+generateGrammarCB :: forall clabel sigma . (Show clabel, Ix sigma, NFData sigma, NFData clabel, Eq clabel)
+    => (Int -> Int -> IO ()) -- ^ callback for reporting progress
+    -> (Array Length Int -> [clabel] -> MulticountDFST sigma -> Vec -> IO ()) -- ^ callback for reporting grammar progress
+    -> Int -- ^ Monte Carlo sample size
+    -> [Double] -- ^ List of accuracy thresholds
+    -> [(clabel, ShortDFST sigma)] -- ^ List of candidate constraints. Labels must be unique. All DFSTs must share the same input bounds.
+    -> [([sigma],Int)] -- ^ Corpus of sample words and their relative frequencies
+    -> IO ([clabel], MulticountDFST sigma, Vec) -- ^ Computed grammar
+generateGrammarCB progresscb grammarcb samplesize thresholds candidates wfs = do
     let lwfs = sortLexicon wfs
         cbound = psegBounds . snd . head $ candidates
-        blankdfa = nildfa cbound
+        blankdfa = pruneAndPack (nildfa cbound)
         lendist = lengthCdf lwfs
+        lenarr = lengthFreqs lwfs
         pwfs = packMultiText cbound (wordFreqs lwfs)
 
-    hashctr :: IORef Int <- newIORef 0
-    let mark500 = do
-            c <- readIORef hashctr
+    passctr :: IORef Int <- newIORef 0
+    candctr :: IORef Int <- newIORef 0
+    let markpass = do
+            modifyIORef' passctr (+1)
+            writeIORef candctr 0
+            p <- readIORef passctr
+            progresscb p 0
+        markcand = do
+            modifyIORef' candctr (+1)
+            c <- readIORef candctr
             when (c `mod` 500 == 0) $ do
+                p <- readIORef passctr
                 hPutStr stderr "#"
                 hFlush stderr
-            modifyIORef' hashctr (+1)
-
-    currentGrammar :: IORef ([clabel], MulticountDFST sigma, Vec) <- newIORef ([],pruneAndPack blankdfa ,zero)
-
-    let genSalad :: IO (PackedText sigma)
-        genSalad = do
-            (_,dfa,weights) <- readIORef currentGrammar
-            salad' <- getStdRandom . runState $ sampleWordSalad (fmap (maxentProb weights) (unpackDFA dfa)) lendist samplesize
-            return . packMultiText cbound . wordFreqs . sortLexicon . fmap (\x -> (x,1)) $ salad'
-
-    currentSalad <- newIORef undefined
+                progresscb p c
+        markprg = do
+            p <- readIORef passctr
+            c <- readIORef candctr
+            progresscb p c
 
-    handle stopsigint $ do
-        forM_ thresholds $ \accuracy -> do
-            putStrLn $ "\n\n\nStarting pass with threshold " ++ showFFloat (Just 3) accuracy ""
-            writeIORef currentSalad =<< genSalad
-            forM_ candidates $ \(cl,cdfa) -> do
-                mark500
-                (grammar, dfa, weights) <- readIORef currentGrammar
-                salad <- readIORef currentSalad
-                let o = fromIntegral $ transducePackedShort cdfa pwfs
-                    o' = fromIntegral $ transducePackedShort cdfa salad
-                    e = o' * fromIntegral (totalWords lwfs) / fromIntegral samplesize
-                    score = upperConfidenceOE o e
+    let genSalad :: MulticountDFST sigma -> Vec -> IO (PackedText sigma)
+        genSalad dfa weights = do
+            salad <- getStdRandom . runState $ sampleWordSalad (fmap (maxentProb weights) (unpackDFA dfa)) lendist samplesize
+            evaluate . packMultiText cbound . wordFreqs . sortLexicon . fmap (\x -> (x,1)) $ salad
 
-                when (score < accuracy && cl `notElem` grammar) $ do
-                    hPutStrLn stderr ""
-                    putStrLn $ "\nSelected Constraint " ++ show cl ++  " (score=" ++ showFFloat (Just 4) score [] ++ ", o=" ++ showFFloat (Just 1) o [] ++ ", e=" ++ showFFloat (Just 1) e [] ++ ")."
-                    let newgrammar = cl:grammar
-                        newdfa :: MulticountDFST sigma = pruneAndPack (rawIntersection consMC (unpackDFA cdfa) (unpackDFA dfa))
-                    putStrLn $ "New grammar has " ++ show (length newgrammar) ++ " constraints and " ++ show (numStates newdfa) ++ " states."
-                    let oldweights = consVec 0 weights
-                    newweights <- evaluate . force $ llpOptimizeWeights (lengthFreqs lwfs) pwfs newdfa oldweights
-                    hPutStrLn stderr ""
-                    putStrLn $ "Recalculated weights: " ++ showFVec (Just 2) newweights
-                    atomicWriteIORef currentGrammar . force $ (newgrammar, newdfa, newweights)
-                    writeIORef currentSalad =<< genSalad
-        putStrLn "\n\n\nAll Pases Complete."
+        processcand :: Double -> (PackedText sigma, [clabel], MulticountDFST sigma, Vec) -> (clabel, ShortDFST sigma) -> IO (PackedText sigma, [clabel], MulticountDFST sigma, Vec)
+        processcand thresh grammar@(salad,rules,dfa,ws) (cl,cdfa) = do
+            markcand
+            let o = fromIntegral $ transducePackedShort cdfa pwfs
+                o' = fromIntegral $ transducePackedShort cdfa salad
+                e = o' * fromIntegral (totalWords lwfs) / fromIntegral samplesize
+            score <- evaluate $ upperConfidenceOE o e
+            if score < thresh && cl `notElem` rules then do
+                markprg
+                hPutStrLn stderr ""
+                putStrLn $ "\nSelected Constraint " ++ show cl ++  " (score=" ++ showFFloat (Just 4) score [] ++ ", o=" ++ showFFloat (Just 1) o [] ++ ", e=" ++ showFFloat (Just 1) e [] ++ ")."
 
-    readIORef currentGrammar
+                let rules' = cl:rules
+                dfa' <- evaluate . pruneAndPack $ rawIntersection consMC (unpackDFA cdfa) (unpackDFA dfa)
+                putStrLn $ "New grammar has " ++ show (length rules') ++ " constraints and " ++ show (numStates dfa') ++ " states."
+                ws' <- evaluate . force $ llpOptimizeWeights (lengthFreqs lwfs) pwfs dfa' (consVec 0 ws)
+                hPutStrLn stderr ""
+                putStrLn $ "Recalculated weights: " ++ showFVec (Just 2) ws'
+                grammarcb lenarr rules' dfa' ws'
+                salad' <- genSalad dfa' ws'
+                return (salad',rules',dfa',ws')
+            else return grammar
 
--- | Given a set of possible segments and a string, break a string into segments.
--- Uses the rules in Fiero orthography (a phonetic writing system using ASCII characters) where the longest possible match is always taken and apostrophes are used as a digraph break.
-segmentFiero :: [String] -- ^ All possible segments
-             -> String -- ^ Raw text
-             -> [String] -- ^ Segmented text
-segmentFiero [] = error "Empty segment list."
-segmentFiero allsegs = go msl where
-    msl = maximum . fmap length $ allsegs
-    go _ [] = []
-    go _ ('\'':xs) = go msl xs
-    go 0 (x:xs) = go msl xs
-    go len xs | seg `elem` allsegs = seg : go msl rest
-              | otherwise = go (len-1) xs
-        where (seg,rest) = splitAt len xs
+        processpass :: (PackedText sigma, [clabel], MulticountDFST sigma, Vec) -> Double -> IO (PackedText sigma, [clabel], MulticountDFST sigma, Vec)
+        processpass grammar thresh = do
+            markpass
+            putStrLn $ "\n\n\nStarting pass with threshold " ++ showFFloat (Just 3) thresh ""
+            foldlM (processcand thresh) grammar candidates
 
--- | Joins segments together using Fiero rules. Inserts apostrophes where necerssary.
-joinFiero :: [String] -- ^ All possible segments
-          -> [String] -- ^ Segmented text
-          -> String -- ^ Raw text
-joinFiero allsegs = go where
-    msl = maximum . fmap length $ allsegs
-    go [] = []
-    go [x] = x
-    go (x:xs@(y:_)) = let z = x++y
-                      in  if any (\s -> isPrefixOf s z && not (isPrefixOf s x)) allsegs
-                          then x ++ ('\'' : go xs)
-                          else x ++ go xs
+    initsalad <- genSalad blankdfa zero
+    let initgrammar = (initsalad,[],blankdfa,zero)
+    (_,finalrules,finaldfa,finalweights) <- foldlM processpass initgrammar thresholds
+    putStrLn "\n\n\nAll Pases Complete."
+    return (finalrules,finaldfa,finalweights)
diff --git a/src/Text/PhonotacticLearner/DFST.hs b/src/Text/PhonotacticLearner/DFST.hs
--- a/src/Text/PhonotacticLearner/DFST.hs
+++ b/src/Text/PhonotacticLearner/DFST.hs
@@ -236,6 +236,7 @@
         tm = fnArray ((1,a),(1,z)) (const (1,mempty))
         fm = fnArray (1,1) (const mempty)
 
+
 -- | Transduce a string of segments where and output the product of the weights (as a Monoid).
 transduceM :: (Ix q, Ix sigma, Monoid k) => DFST q sigma k -> [sigma] -> k
 transduceM (DFST q0 tm fw) cs = mconcat ws <> (fw ! fq)
@@ -246,6 +247,8 @@
 transduceR (DFST q0 tm fw) cs = productR ws ⊗ (fw ! fq)
     where (fq, ws) = mapAccumL (curry (tm!)) q0 cs
 
+{-# INLINABLE transduceM #-}
+{-# INLINABLE transduceR #-}
 
 
 ------------------------------------------------------------------------------
@@ -572,8 +575,8 @@
 type SegSet sigma = UArray sigma Bool
 
 -- | Glob of segment lists, nore generalized version of ngrams allowing for repeated classes as well as single ones. The two boolean parameters restrict the glob to match a prefixes or suffixes only.
-data ListGlob sigma = ListGlob Bool -- Is restricted to string start
-                               Bool -- Is restricted to string end
+data ListGlob sigma = ListGlob {-# UNPACK #-} !Bool -- Is restricted to string start
+                               {-# UNPACK #-} !Bool -- Is restricted to string end
                                [(GlobReps, SegSet sigma)] -- List of character sets and their quantifiers.
                                deriving (Eq, Ord)
 
diff --git a/src/Text/PhonotacticLearner/PhonotacticConstraints.hs b/src/Text/PhonotacticLearner/PhonotacticConstraints.hs
--- a/src/Text/PhonotacticLearner/PhonotacticConstraints.hs
+++ b/src/Text/PhonotacticLearner/PhonotacticConstraints.hs
@@ -14,11 +14,11 @@
 
 module Text.PhonotacticLearner.PhonotacticConstraints (
     -- * Phonological Features
-    FeatureState(..), SegRef,
+    FeatureState(..), SegRef(..),
 
     FeatureTable(..), srBounds, ftlook,
     segsToRefs, refsToSegs,
-    csvToFeatureTable,
+    csvToFeatureTable, featureTableToCsv,
 
     -- * Natural Classes
     NaturalClass(..), classToSeglist,
@@ -32,7 +32,7 @@
 import Text.PhonotacticLearner.MaxentGrammar
 import Text.PhonotacticLearner.DFST
 
-import Text.CSV
+import Text.Read.CSV
 import Data.Array.IArray
 import Data.Maybe
 import Data.Tuple
@@ -46,7 +46,7 @@
 import qualified Data.Text as T
 import qualified Data.Map.Lazy as M
 import Text.ParserCombinators.ReadP
-import Text.Read(Read(..),lift,parens)
+import Text.Read(Read(..),lift,parens,readMaybe)
 
 
 -- | Enumeration for feature states (can be +,-,0)
@@ -62,8 +62,11 @@
                                        , featNames :: Array Int T.Text
                                        , segNames :: Array SegRef sigma
                                        , featLookup :: M.Map T.Text Int
-                                       , segLookup :: M.Map sigma SegRef } deriving (Show)
+                                       , segLookup :: M.Map sigma SegRef } deriving (Show, Eq)
 
+instance (Ord a, NFData a) => NFData (FeatureTable a) where
+    rnf (FeatureTable ft fn sn fl sl) = rnf ft `seq` rnf fn `seq` rnf sn `seq` rnf fl `seq` rnf sl
+
 -- | Bounds for segment references
 srBounds :: FeatureTable sigma -> (SegRef, SegRef)
 srBounds ft = bounds (segNames ft)
@@ -74,7 +77,7 @@
 ftlook ft sr fi = featTable ft ! (sr,fi)
 {-# INLINE ftlook #-}
 
--- | Convert a string of raw segments to a string of 'SegRef's
+-- | Convert a string of raw segments to a string of 'SegRef's. Skips unrecognisable segments.
 segsToRefs :: (Ord sigma) => FeatureTable sigma -> [sigma] -> [SegRef]
 segsToRefs ft = mapMaybe (\x -> M.lookup x (segLookup ft))
 
@@ -109,7 +112,7 @@
 -}
 csvToFeatureTable :: (Ord sigma) => (String -> sigma) -> String -> Maybe (FeatureTable sigma)
 csvToFeatureTable readSeg rawcsv = do
-    Right parsedcsv <- return (parseCSV "" rawcsv)
+    parsedcsv <- readCSV rawcsv
     ((_:segcells) : rawfeatrecs) <- return (fmap (fmap lstrip) parsedcsv)
     let numsegs  = length segcells
     guard (numsegs > 0)
@@ -134,15 +137,40 @@
     guard (M.size segmap == rangeSize (bounds seglist))
     return (FeatureTable ft featlist seglist featmap segmap)
 
+fschar FPlus = "+"
+fschar FMinus = "-"
+fschar FOff = "0"
 
+-- | Cave a modified feature table to CSV format
+featureTableToCsv :: (sigma -> String) -> FeatureTable sigma -> String
+featureTableToCsv writeSeg ft = writeCSV (header : body) where
+    header = "" : fmap writeSeg (elems (segNames ft))
+    body = [T.unpack fn : [fschar (ftlook ft s f) | s <- indices (segNames ft)] | (f,fn) <- assocs (featNames ft)]
+
+-- | Parse a list of strings (one per line) into a list of SegRef strings and frequencies.
+-- Takes a feature table and a function to divide strings into segments (for single character segments, @fmap return@ may be used).
+-- Lines in the list may be optionally followed by a tab and an integer indicating their frequendy (instead of suplicating lines).play table
+
+readSrLexicon :: FeatureTable String -> (String -> [String]) -> String -> [([SegRef],Int)]
+readSrLexicon ft seg text = do
+    line <- lines text
+    let (wt@(_:_),wf') = break (== '\t') line
+    n <- case (words wf') of
+            [] -> [1]
+            [wf] -> maybeToList $ readMaybe wf
+            _ -> []
+    return (segsToRefs ft (seg wt), n)
+
+
 --------------------------------------------------------------------------------
 
 -- | Representation of a natural class as a list of features and their states. Can ahso handle inverted classes.
-data NaturalClass = NClass { isInverted :: Bool
+data NaturalClass = NClass { isInverted :: {-# UNPACK #-} !Bool
                            , featureList :: [(FeatureState, T.Text)]
                            } deriving (Eq, Ord)
+
 instance NFData NaturalClass where
-    rnf c@(NClass b fs) = b `seq` rnf fs
+    rnf (NClass b fs) = b `seq` rnf fs
 
 -- | Uses SPE format
 instance Show NaturalClass where
@@ -185,7 +213,7 @@
             return (s,fi)
 
 -- | Globs using 'NaturalClass' instead of 'SegSet'
-data ClassGlob = ClassGlob Bool Bool [(GlobReps, NaturalClass)] deriving (Eq, Ord)
+data ClassGlob = ClassGlob {-# UNPACK #-} !Bool {-# UNPACK #-} !Bool [(GlobReps, NaturalClass)] deriving (Eq, Ord)
 instance NFData ClassGlob where
     rnf (ClassGlob isinit isfin gparts) = isinit `seq` isfin `seq` rnf gparts
 
diff --git a/src/Text/PhonotacticLearner/PhonotacticConstraints/FileFormats.hs b/src/Text/PhonotacticLearner/PhonotacticConstraints/FileFormats.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/PhonotacticLearner/PhonotacticConstraints/FileFormats.hs
@@ -0,0 +1,136 @@
+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, ParallelListComp #-}
+
+{-|
+Module: Text.PhonotacticLearner.PhonotacticConstraints.FileFormats
+Description: Generation of candidate constraint sets.
+Copyright: © 2016-2017 George Steel and Peter Jurgec
+License: GPL-2+
+Maintainer: george.steel@gmail.com
+
+Functions for saving and loading lexicons and 'ClassGlob' constraint grammars in standard formats.
+-}
+
+module Text.PhonotacticLearner.PhonotacticConstraints.FileFormats where
+
+import Control.Monad
+import Data.Traversable
+import Data.Monoid
+import Data.Foldable
+import Text.Read
+import Data.List
+import Data.Maybe
+import qualified Data.Text as T
+import qualified Data.Set as S
+import qualified Data.Map as M
+import Data.Array.IArray
+import Numeric
+import Control.DeepSeq
+
+import Text.PhonotacticLearner.PhonotacticConstraints
+import Text.PhonotacticLearner.MaxentGrammar
+import Text.PhonotacticLearner.Util.Ring
+
+
+-- | Given a set of possible segments and a string, break a string into segments.
+-- Uses the rules in Fiero orthography (a phonetic writing system using ASCII characters) where the longest possible match is always taken and apostrophes are used as a digraph break.
+segmentFiero :: S.Set String -- ^ All possible segments
+             -> String -- ^ Raw text
+             -> [String] -- ^ Segmented text
+--segmentFiero [] = error "Empty segment list."
+segmentFiero allsegs = go msl where
+    msl = maximum . S.map length $ allsegs
+    go _ [] = []
+    go _ ('\'':xs) = go msl xs
+    go 0 (x:xs) = go msl xs
+    go len xs | S.member seg allsegs = seg : go msl rest
+              | otherwise = go (len-1) xs
+        where (seg,rest) = splitAt len xs
+
+-- | Joins segments together using Fiero rules. Inserts apostrophes where necerssary.
+joinFiero :: S.Set String -- ^ All possible segments
+          -> [String] -- ^ Segmented text
+          -> String -- ^ Raw text
+joinFiero allsegs = go where
+    msl = maximum . S.map length $ allsegs
+    go [] = []
+    go [x] = x
+    go (x:xs@(y:_)) = let z = x++y
+                      in  if any (\s -> isPrefixOf s z && not (isPrefixOf s x)) allsegs
+                          then x ++ ('\'' : go xs)
+                          else x ++ go xs
+
+-- | Structure for reperesenting lexicon entries
+data LexRow = LexRow [String] Int
+
+-- | Parse a lexicon from a file. Segmentation of a word uses fiero rules (which will also decode space-separated segments and single-character segments).
+-- Words may optionally be followed by a tab character and an integer indicating frequency (1 by default).
+parseWordlist :: S.Set String -> T.Text -> [LexRow]
+parseWordlist segs rawlist = do
+    line <- T.lines rawlist
+    let (rawword : rest) = T.split (== '\t') line
+        w = segmentFiero segs (T.unpack rawword)
+        fr = fromMaybe 1 $ do
+            [f] <- return (rest >>= T.words)
+            readMaybe (T.unpack f)
+    guard (w /= [])
+    return $ LexRow w fr
+
+-- | Collate a list of words and frequencies from raw phonetic text.
+collateWordlist :: S.Set String -> T.Text -> [LexRow]
+collateWordlist segs rawtext = fmap (uncurry LexRow) . M.assocs . M.fromListWith (+) $ do
+    rawword <- T.words rawtext
+    let w = segmentFiero segs (T.unpack rawword)
+    guard (w /= [])
+    return (w, 1)
+
+-- | Serializes a list of words and frequerncies to a string for decoding with 'parseWordlist'. Connects segments using Fiero rules.
+serWordlist :: S.Set String -> [LexRow] -> T.Text
+serWordlist segs = T.unlines . mapMaybe showRow where
+    showRow (LexRow _ n) | n <= 0 = Nothing
+    showRow (LexRow w 1) = Just . T.pack $ joinFiero segs w
+    showRow (LexRow w n) = Just . T.pack $ joinFiero segs w ++ "\t" ++ show n
+
+-- | Serializes a list of words and frequerncies to a string for decoding with 'parseWordlist'. Puts spaces between segments.
+serWordlistSpaced :: [LexRow] -> T.Text
+serWordlistSpaced = T.unlines . mapMaybe showRow where
+    showRow (LexRow _ n) | n <= 0 = Nothing
+    showRow (LexRow w 1) = Just . T.pack $ unwords w
+    showRow (LexRow w n) = Just . T.pack $ unwords w ++ "\t" ++ show n
+
+-- | Reperesentation of a 'ClassGlob' grammar.
+data PhonoGrammar = PhonoGrammar {
+    lengthDist :: (Array Length Int), -- ^ Distribution of word lengths
+    constraintSet :: [ClassGlob], -- ^ Set of constraints
+    weightSet :: Vec -- ^ Set of weights in same order as constraints
+} deriving (Eq, Show)
+
+instance NFData PhonoGrammar where
+    rnf (PhonoGrammar lendist grammar weights) = rnf lendist `seq` rnf grammar `seq` rnf weights
+
+-- | Parse a grammar from a file. Blank lines ans lines begining with # are ignored.
+-- The first regular line must contain a list of (Length,Int) pairs and subsequent lines must contain a weight followed by a ClassGlob.
+parseGrammar :: T.Text -> Maybe PhonoGrammar
+parseGrammar rawgrammar = do
+    let noncomment l = not (T.null l) && (T.head l /= '#')
+    (fline:glines) <- return $ filter noncomment (T.lines rawgrammar)
+    lenlist <- readMaybe (T.unpack fline)
+    let maxlen = maximum (fmap fst lenlist)
+        lenarr = accumArray (+) 0 (1,maxlen) (filter ((> 0) . fst) lenlist)
+        readline l = do
+            let (wt, ct') = T.breakOn " " l
+            w::Double <- readMaybe (T.unpack wt)
+            (' ', ct) <- T.uncons ct'
+            c::ClassGlob <- readMaybe (T.unpack ct)
+            return (c,w)
+    cs <- traverse readline (reverse glines)
+    return $ PhonoGrammar lenarr (fmap fst cs) (vec (fmap snd cs))
+
+-- | Serialize a grammar without length distribution
+serGrammarRules :: [ClassGlob] -> Vec -> T.Text
+serGrammarRules grammar weights =
+    (T.unlines . reverse) [T.pack $ showFFloat (Just 3) w "  " ++ show c | c <- grammar | w <- coords weights]
+
+-- | Serialize a grammar including length distribution
+serGrammar :: PhonoGrammar -> T.Text
+serGrammar (PhonoGrammar lendist grammar weights) =
+    "# Length Distribution:\n" <> (T.pack . show . assocs $ lendist) <> "\n\n# Constraints:\n" <> serGrammarRules grammar weights
diff --git a/src/Text/PhonotacticLearner/PhonotacticConstraints/Generators.hs b/src/Text/PhonotacticLearner/PhonotacticConstraints/Generators.hs
--- a/src/Text/PhonotacticLearner/PhonotacticConstraints/Generators.hs
+++ b/src/Text/PhonotacticLearner/PhonotacticConstraints/Generators.hs
@@ -5,15 +5,14 @@
 License: GPL-2+
 Maintainer: george.steel@gmail.com
 
-Functions for generating sets of candidate constraint sets.
-For efficiency, classes are reperesented as @('NaturalClass', 'SegSet' 'SegRef')@ pairs and
-constraints are output as @('ClassGlob', 'ListGlob' 'SegRef')@ pairs, avoiding the need for repeated conversions and copying of classes.
+Functions for generating sets of candidate constraint sets. For basic use, 'CandidateSettings' and 'CandidateGrammar' while the other functions provide more fine-grained control.
 
-The 'classesByGenreraity' function enumerates the classes defined by a feature table in a sensible order, removing duplicate descriptions of the same class. The ug functions then take these classes and then combine them imto globs in various ways.
+The 'classesByGenreraity' function enumerates the classes defined by a feature table in a sensible order, removing duplicate descriptions of the same class. The ug functions then take these classes and then combine them imto globs in various ways. For efficiency, classes are reperesented as @('NaturalClass', 'SegSet' 'SegRef')@ pairs and constraints are output as @('ClassGlob', 'ListGlob' 'SegRef')@ pairs, avoiding the need for repeated conversions and copying of classes.
 
 -}
 
 module Text.PhonotacticLearner.PhonotacticConstraints.Generators (
+    CandidateSettings(..), candidateGrammar,
     ngrams,
     classesByGenerality,
     ugSingleClasses, ugBigrams,
@@ -25,48 +24,98 @@
 
 import Text.PhonotacticLearner.PhonotacticConstraints
 import Text.PhonotacticLearner.DFST
+import Data.Bits
 import Data.List
 import Data.Array.IArray
-import qualified Data.Map as M
+import qualified Data.Map.Strict as M
+import qualified Data.Text as T
+import qualified Data.Set as S
 import Control.Monad
 import Control.DeepSeq
+import Control.Parallel
 
+-- | Settings for grammar generation
+data CandidateSettings = CandidateSettings {
+    useEdges :: Bool, -- ^ Allow single classes and bigrams restricted to word boundaries.
+    useTrigrams :: Maybe [T.Text], -- ^ Allows trigrams as long as at least one class is [] or [±x] where x is in the included list.
+    useBroken :: Maybe [T.Text] -- ^ Allows long-distance constraints of the form AB+C where A,C are classes and C = [] or [±x] with x in the list.
+} deriving (Eq, Show)
+
+-- | Generate a reasonable set of candidate constraints based single classes, bigrams, and the4 additionsl constraint types specified in the settings.
+-- First and second return values are the number of classes and candidates in the grammar, and the third is the set of candidates.
+candidateGrammar :: FeatureTable sigma -> CandidateSettings -> (Int , Int, [(ClassGlob, ListGlob SegRef)])
+candidateGrammar ft (CandidateSettings edges mtri mbroken) = ncls `seq` rnf candidates `seq` (ncls, ncand, candidates) where
+    cls = classesByGenerality ft 3
+    ncls = length cls
+    cand1 = ugSingleClasses cls
+    cande1 = if edges then ugEdgeClasses cls else []
+    cand2 = ugBigrams cls
+    cande2 = if edges then ugEdgeBigrams cls else []
+    cand3 = case mtri of
+        Nothing -> []
+        Just tri -> ugLimitedTrigrams cls (coreClassesFromFeats ft tri)
+    candb = case mbroken of
+        Nothing -> []
+        Just broken -> ugLongDistance cls (coreClassesFromFeats ft broken)
+    candidates = cls `pseq` (rnf cande2 `par` rnf cand3 `par` rnf candb `par` join [cand1,cande1,cand2,cande2,cand3,candb])
+    ncand = length candidates
+
 -- | Given a number n and a sequence, returns all subsewuences of length n.
 ngrams  :: Int -> [a] -> [[a]]
 ngrams  0  _       = [[]]
 ngrams  _  []      = []
 ngrams  n  (x:xs)  = fmap (x:) (ngrams (n-1) xs) ++ ngrams n xs
 
+segsetFromInteger :: (SegRef,SegRef) -> Integer -> SegSet SegRef
+segsetFromInteger b set = fnArray b (\(Seg i) -> testBit set i)
+
+
 -- | Enumerate all classes (and their inverses) to a certain number of features
 -- in descending order of the number of segments the uninverted class contains.
 -- Discards duplicates (having the same set of segments).
 --
 -- Each segment is returned as a tripple with the (negated for sorting) numbet of segments in the class, the class label, and the set of segments it contains.
 classesByGenerality :: FeatureTable sigma -> Int -> [(Int, (NaturalClass, SegSet SegRef))]
-classesByGenerality ft maxfeats = force $ fmap (\((ns, cs), c) -> (ns,(c,cs))) (M.assocs cls)
+classesByGenerality ft maxfeats = force . fmap prepout $ (M.assocs cls)
     where
-        cls = M.fromListWith (const id) $ do
+        prepout ((n, set), cls)= (n, (cls, segsetFromInteger b set))
+        b = (srBounds ft)
+        sr = range b
+        mask = foldl' (.|.) zeroBits [bit i | Seg i <- sr]
+        fsets = do
+            (fi,fn) <- assocs (featNames ft)
+            fs <- [FPlus, FMinus]
+            let fset = foldl' (.|.) 0 [bit i | s@(Seg i) <- sr, ftlook ft s fi == fs]
+            return ((fs,fn),fset)
+        cls = M.fromListWith (const id) . force $ do
             isInv <- [False,True]
             nf <- range (0, maxfeats)
-            fs <- ngrams nf (elems (featNames ft))
-            c <- fmap (NClass isInv) . forM fs $ \f -> [(FPlus,f), (FMinus,f)]
-            let cs = classToSeglist ft c
-            let ns = length . filter id . elems $ cs
-            guard (ns /= 0)
-            return ((negate ns, cs), c)
+            (fs,sets) <- fmap unzip (ngrams nf fsets)
+            let cls = NClass isInv fs
+                set = (if isInv then mask else 0) `xor` (foldl' (.&.) mask sets)
+                ns = popCount set
+            guard (set /= 0)
+            return ((negate ns, set), cls)
 
+coreClassesFromFeats :: FeatureTable sigma -> [T.Text] -> [(NaturalClass, SegSet SegRef)]
+coreClassesFromFeats ft feats = nubBy (\x y -> snd x == snd y) $ do
+    c <- fmap (NClass False) $ [] : (curry return <$> [FPlus,FMinus] <*> feats)
+    let sl = classToSeglist ft c
+    guard $ or (elems sl)
+    return (c,sl)
+
 -- | Given a set of classes, return a set of globs matching those classes.
 ugSingleClasses :: [(Int, (NaturalClass, SegSet SegRef))] -> [(ClassGlob, ListGlob SegRef)]
-ugSingleClasses cls = fmap snd . sortOn fst $ do
+ugSingleClasses cls = fmap snd . sortOn fst . force $ do
     (w,(c,l)) <- cls
     guard (not (isInverted c))
     let g = ClassGlob False False [(GSingle,c)]
         lg = ListGlob False False [(GSingle,l)]
     return (w,(g,lg))
 
--- Given a set of classes, return a set of globs matching those globs at word boundaries. At most one class may be inverted.
+-- | Given a set of classes, return a set of globs matching those globs at word boundaries. At most one class may be inverted.
 ugEdgeClasses :: [(Int, (NaturalClass, SegSet SegRef))] -> [(ClassGlob, ListGlob SegRef)]
-ugEdgeClasses cls = fmap snd . sortOn fst $ do
+ugEdgeClasses cls = fmap snd . sortOn fst . force $ do
     (w,(c,l)) <- cls
     guard (not (isInverted c))
     (isinit,isfin) <- [(False,True),(True,False)]
@@ -76,7 +125,7 @@
 
 -- | Given a set of classes, return a set pf globs matching class pairs, ordered by total weight. At most one class may be inverted.
 ugBigrams :: [(Int, (NaturalClass, SegSet SegRef))] -> [(ClassGlob, ListGlob SegRef)]
-ugBigrams cls = fmap snd . sortOn fst $ do
+ugBigrams cls = fmap snd . sortOn fst . force $ do
     (w1,(c1,l1)) <- cls
     (w2,(c2,l2)) <- cls
     guard (not (isInverted c1 && isInverted c2))
@@ -86,7 +135,7 @@
 
 -- | Given a set of classes, return a set pf globs matching class pairs at word boundaries, ordered by total weight. At most one class may be inverted.
 ugEdgeBigrams :: [(Int, (NaturalClass, SegSet SegRef))] -> [(ClassGlob, ListGlob SegRef)]
-ugEdgeBigrams cls = fmap snd . sortOn fst $ do
+ugEdgeBigrams cls = fmap snd . sortOn fst . force $ do
     (w1,(c1,l1)) <- cls
     (w2,(c2,l2)) <- cls
     guard (not (isInverted c1 && isInverted c2))
@@ -97,15 +146,16 @@
 
 -- | Given a set of classes ansd a smaller subset, return a set of globs matching trigrams of classes from the set where at least one class is contained in the subset.  At most one class may be inverted.
 ugLimitedTrigrams :: [(Int, (NaturalClass, SegSet SegRef))] -> [(NaturalClass, SegSet SegRef)] -> [(ClassGlob, ListGlob SegRef)]
-ugLimitedTrigrams cls rcls = fmap snd . sortOn fst $ do
+ugLimitedTrigrams cls rcls = fmap snd . sortOn fst . force $ do
+    let rcls' = S.fromList (fmap fst rcls)
     (w1,(c1,l1)) <- cls
     (w2,(c2,l2)) <- cls
     (w,(c3,l3)) <- case () of
-         () | (c1,l1) `elem` rcls -> do
+         () | S.member c1 rcls' -> do
                 (w3,(c3',l3')) <- cls
                 guard (not (isInverted c2 && isInverted c3'))
                 return (w2+w3, (c3',l3'))
-            | (c2,l2) `elem` rcls -> do
+            | S.member c2 rcls' -> do
                 (w3,(c3',l3')) <- cls
                 guard (not (isInverted c1 && isInverted c3'))
                 return (w1+w3, (c3',l3'))
@@ -120,7 +170,7 @@
 -- | Given two sets of classes, return globs matching a pair oc slasses in the first set separated by any number of occurrences of a class in the second set.  At most one class may be inverted. At most one class may be inverted.
 -- This can lead to fairly large grammar DFAs when multiple such constraints are merged.
 ugLongDistance :: [(Int, (NaturalClass, SegSet SegRef))] -> [(NaturalClass, SegSet SegRef)] -> [(ClassGlob, ListGlob SegRef)]
-ugLongDistance cls rcls = fmap snd . sortOn fst $ do
+ugLongDistance cls rcls = fmap snd . sortOn fst . force $ do
     (w1,(c1,l1)) <- cls
     (c2,l2) <- rcls
     (w3,(c3,l3)) <- cls
diff --git a/src/Text/PhonotacticLearner/Util/Probability.hs b/src/Text/PhonotacticLearner/Util/Probability.hs
--- a/src/Text/PhonotacticLearner/Util/Probability.hs
+++ b/src/Text/PhonotacticLearner/Util/Probability.hs
@@ -80,11 +80,13 @@
 instance (RingModule Double v) => Additive (Expectation v) where
     zero = Exp 0 zero
     (Exp p1 v1) ⊕ (Exp p2 v2) = Exp (p1 + p2) (v1 ⊕ v2)
+    {-# INLINE (⊕) #-}
 
 -- intersect independent events or combine event with conditional probability
 instance (RingModule Double v) => Semiring (Expectation v) where
     one = Exp 1 zero
     (Exp p1 v1) ⊗ (Exp p2 v2) = Exp (p1 * p2) ((p1 ⊙ v2) ⊕ (p2 ⊙ v1))
+    {-# INLINE (⊗) #-}
 
 -- | Get the expectation conditional on the event actually occurring.
 normalizeExp :: (RingModule Double v) => Expectation v -> v
diff --git a/src/Text/PhonotacticLearner/Util/Ring.hs b/src/Text/PhonotacticLearner/Util/Ring.hs
--- a/src/Text/PhonotacticLearner/Util/Ring.hs
+++ b/src/Text/PhonotacticLearner/Util/Ring.hs
@@ -49,6 +49,7 @@
     addinv :: g -> g -- ^ Additive inverse, more general version of 'negate'
     (⊖) :: g -> g -> g -- ^ Subtraction
     x ⊖ y = x ⊕ addinv y
+    {-# INLINE (⊖) #-}
 
 -- | Semirings with addition and multiplication (but not subtraction).
 class (Additive r) => Semiring r where
@@ -91,12 +92,16 @@
 instance {-# OVERLAPPABLE #-} (Num r) => Additive r where
     zero = 0
     (⊕) = (+)
+    {-# INLINE (⊕) #-}
 instance {-# OVERLAPPABLE #-} (Num r) => Semiring r where
     one = 1
     (⊗) = (*)
+    {-# INLINE (⊗) #-}
 instance {-# OVERLAPPABLE #-} (Num r) => AdditiveGroup r where
     addinv = negate
+    {-# INLINE addinv #-}
     (⊖) = (-)
+    {-# INLINE (⊖) #-}
 instance {-# OVERLAPPABLE #-} (Num r) => Ring r
 
 
@@ -147,6 +152,7 @@
 -- | Since ℝ is a module over ℤ, using scalar multiplication can save a lot of coersion noise.
 instance RingModule Int Double where
     x ⊙ y = fromIntegral x * y
+    {-# INLINE (⊙) #-}
 
 -- | Implements variable-length vectors. Addition batween vectors of different lengths occurs by letting ℝ⊆ℝ²⊆ℝ³⊆… by embedding each length in the space op polynomials.
 newtype Vec = Vec {unVec :: V.Vector Double} deriving (Eq, Read, Show, NFData)
@@ -176,11 +182,14 @@
 
 instance AdditiveGroup Vec where
     addinv (Vec xs) = Vec (V.map negate xs)
+    {-# INLINE addinv #-}
 
 instance RingModule Double Vec where
     a ⊙ (Vec xs) = Vec (V.map (a *) xs)
+    {-# INLINE (⊙) #-}
 instance RingModule Int Vec where
     a ⊙ (Vec xs) = Vec (V.map (fromIntegral a *) xs)
+    {-# INLINE (⊙) #-}
 
 -- | Standard inner product on ℝⁿ.
 innerProd :: Vec -> Vec -> Double
