packages feed

brassica 1.0.0 → 1.1.0

raw patch · 19 files changed

+626/−396 lines, 19 filesdep ~aesondep ~fast-myers-diffdep ~file-embedPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, fast-myers-diff, file-embed, megaparsec, optparse-applicative, pandoc, parallel, parser-combinators, tasty

API changes (from Hackage documentation)

- Brassica.Paradigm: Always :: Condition
- Brassica.Paradigm: newtype FeatureName
- Brassica.SoundChange.Frontend.Internal: ReportRulesApplied :: ApplicationMode
- Brassica.SoundChange.Frontend.Internal: instance GHC.Enum.Enum Brassica.SoundChange.Frontend.Internal.HighlightMode
- Brassica.SoundChange.Frontend.Internal: instance GHC.Enum.Enum Brassica.SoundChange.Frontend.Internal.InputLexiconFormat
- Brassica.SoundChange.Frontend.Internal: instance GHC.Enum.Enum Brassica.SoundChange.Frontend.Internal.OutputMode
+ Brassica.Paradigm: All :: [Expr a] -> Expr a
+ Brassica.Paradigm: Any :: [Expr a] -> Expr a
+ Brassica.Paradigm: Class :: FeatureName
+ Brassica.Paradigm: Pred :: a -> Expr a
+ Brassica.Paradigm: Stem :: Int -> Process
+ Brassica.Paradigm: data Expr a
+ Brassica.Paradigm: data FeatureName
+ Brassica.Paradigm: parseStems :: String -> (Maybe String, [String])
+ Brassica.SoundChange.Apply: ActionNotApplied :: r -> PWord -> LogItem r
+ Brassica.SoundChange.Apply: AllChanged :: HighlightMode
+ Brassica.SoundChange.Apply: SpecificRule :: HighlightMode
+ Brassica.SoundChange.Apply: data HighlightMode
+ Brassica.SoundChange.Apply: rulesNotApplied :: Log r -> [r]
+ Brassica.SoundChange.Apply.Internal: ActionNotApplied :: r -> PWord -> LogItem r
+ Brassica.SoundChange.Apply.Internal: AllChanged :: HighlightMode
+ Brassica.SoundChange.Apply.Internal: SpecificRule :: HighlightMode
+ Brassica.SoundChange.Apply.Internal: data HighlightMode
+ Brassica.SoundChange.Apply.Internal: instance GHC.Classes.Eq Brassica.SoundChange.Apply.Internal.HighlightMode
+ Brassica.SoundChange.Apply.Internal: instance GHC.Show.Show Brassica.SoundChange.Apply.Internal.HighlightMode
+ Brassica.SoundChange.Apply.Internal: rulesNotApplied :: Log r -> [r]
+ Brassica.SoundChange.Expand: instance GHC.Show.Show Brassica.SoundChange.Expand.ExpansionState
+ Brassica.SoundChange.Frontend.Internal: NotAppliedRulesList :: [Rule Expanded] -> ApplicationOutput a r
+ Brassica.SoundChange.Frontend.Internal: ReportApplied :: ReportMode
+ Brassica.SoundChange.Frontend.Internal: ReportNotApplied :: ReportMode
+ Brassica.SoundChange.Frontend.Internal: ReportRules :: ReportMode -> ApplicationMode
+ Brassica.SoundChange.Frontend.Internal: ReqParadigm :: String -> String -> Bool -> Int -> Request
+ Brassica.SoundChange.Frontend.Internal: ReqRules :: String -> String -> Maybe ReportMode -> InputLexiconFormat -> HighlightMode -> OutputMode -> Maybe [Component PWord] -> String -> Int -> Request
+ Brassica.SoundChange.Frontend.Internal: RespError :: [Int] -> String -> Response
+ Brassica.SoundChange.Frontend.Internal: RespNotApplied :: [Int] -> Response
+ Brassica.SoundChange.Frontend.Internal: RespParadigm :: String -> Response
+ Brassica.SoundChange.Frontend.Internal: RespRules :: Maybe [Component PWord] -> String -> Response
+ Brassica.SoundChange.Frontend.Internal: [changes] :: Request -> String
+ Brassica.SoundChange.Frontend.Internal: [highlights] :: Response -> [Int]
+ Brassica.SoundChange.Frontend.Internal: [hlMode] :: Request -> HighlightMode
+ Brassica.SoundChange.Frontend.Internal: [inFmt] :: Request -> InputLexiconFormat
+ Brassica.SoundChange.Frontend.Internal: [input] :: Request -> String
+ Brassica.SoundChange.Frontend.Internal: [message] :: Response -> String
+ Brassica.SoundChange.Frontend.Internal: [outMode] :: Request -> OutputMode
+ Brassica.SoundChange.Frontend.Internal: [output] :: Response -> String
+ Brassica.SoundChange.Frontend.Internal: [pText] :: Request -> String
+ Brassica.SoundChange.Frontend.Internal: [prev] :: Response -> Maybe [Component PWord]
+ Brassica.SoundChange.Frontend.Internal: [report] :: Request -> Maybe ReportMode
+ Brassica.SoundChange.Frontend.Internal: [reqTimeout] :: Request -> Int
+ Brassica.SoundChange.Frontend.Internal: [sep] :: Request -> String
+ Brassica.SoundChange.Frontend.Internal: [separateLines] :: Request -> Bool
+ Brassica.SoundChange.Frontend.Internal: data ReportMode
+ Brassica.SoundChange.Frontend.Internal: data Request
+ Brassica.SoundChange.Frontend.Internal: data Response
+ Brassica.SoundChange.Frontend.Internal: dispatch :: Request -> Response
+ Brassica.SoundChange.Frontend.Internal: getErrorLocs :: ParseErrorBundle String Void -> [Int]
+ Brassica.SoundChange.Frontend.Internal: instance Control.DeepSeq.NFData Brassica.SoundChange.Frontend.Internal.Response
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.FromJSON.FromJSON Brassica.SoundChange.Frontend.Internal.HighlightMode
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.FromJSON.FromJSON Brassica.SoundChange.Frontend.Internal.InputLexiconFormat
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.FromJSON.FromJSON Brassica.SoundChange.Frontend.Internal.OutputMode
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.FromJSON.FromJSON Brassica.SoundChange.Frontend.Internal.ReportMode
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.FromJSON.FromJSON Brassica.SoundChange.Frontend.Internal.Request
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.FromJSON.FromJSON Brassica.SoundChange.Frontend.Internal.Response
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.ToJSON.ToJSON Brassica.SoundChange.Frontend.Internal.HighlightMode
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.ToJSON.ToJSON Brassica.SoundChange.Frontend.Internal.InputLexiconFormat
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.ToJSON.ToJSON Brassica.SoundChange.Frontend.Internal.OutputMode
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.ToJSON.ToJSON Brassica.SoundChange.Frontend.Internal.ReportMode
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.ToJSON.ToJSON Brassica.SoundChange.Frontend.Internal.Request
+ Brassica.SoundChange.Frontend.Internal: instance Data.Aeson.Types.ToJSON.ToJSON Brassica.SoundChange.Frontend.Internal.Response
+ Brassica.SoundChange.Frontend.Internal: instance GHC.Classes.Eq Brassica.SoundChange.Frontend.Internal.ReportMode
+ Brassica.SoundChange.Frontend.Internal: instance GHC.Generics.Generic Brassica.SoundChange.Frontend.Internal.Response
+ Brassica.SoundChange.Frontend.Internal: instance GHC.Show.Show Brassica.SoundChange.Frontend.Internal.ReportMode
+ Brassica.SoundChange.Frontend.Internal: instance GHC.Show.Show Brassica.SoundChange.Frontend.Internal.Request
+ Brassica.SoundChange.Frontend.Internal: instance GHC.Show.Show Brassica.SoundChange.Frontend.Internal.Response
+ Brassica.SoundChange.Frontend.Internal: parFmap :: (a -> b) -> [Component a] -> [Component b]
+ Brassica.SoundChange.Tokenise: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Brassica.SoundChange.Tokenise.Component a)
+ Brassica.SoundChange.Tokenise: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Brassica.SoundChange.Tokenise.Component a)
+ Brassica.SoundChange.Types: DisablePersistent :: String -> Directive
+ Brassica.SoundChange.Types: [highlightSpecificRule] :: Flags -> Bool
+ Brassica.SoundChange.Types: [loc] :: Rule (c :: LexemeType -> Type) -> Int
+ Brassica.SoundChange.Types: [persistent] :: Flags -> Maybe String
- Brassica.Paradigm: Feature :: Condition -> Maybe FeatureName -> [Grammeme] -> Feature
+ Brassica.Paradigm: Feature :: Expr Condition -> Maybe String -> [Grammeme] -> Feature
- Brassica.Paradigm: NewMapping :: [AbstractGrammeme] -> Affix -> Statement
+ Brassica.Paradigm: NewMapping :: Expr AbstractGrammeme -> Affix -> Statement
- Brassica.Paradigm: applyParadigm :: Paradigm -> String -> ResultsTree String
+ Brassica.Paradigm: applyParadigm :: Paradigm -> Maybe String -> [String] -> ResultsTree String
- Brassica.SoundChange.Apply: getChangedOutputs :: Log (Statement c d) -> Maybe (PWord, Bool)
+ Brassica.SoundChange.Apply: getChangedOutputs :: forall (c :: LexemeType -> Type) d. HighlightMode -> Log (Statement c d) -> Maybe (PWord, Bool)
- Brassica.SoundChange.Apply: getChangedReports :: Log (Statement c d) -> [(PWord, Bool)]
+ Brassica.SoundChange.Apply: getChangedReports :: forall (c :: LexemeType -> Type) d. HighlightMode -> Log (Statement c d) -> [(PWord, Bool)]
- Brassica.SoundChange.Apply.Internal: SuccessEpenthesis :: RuleStatus
+ Brassica.SoundChange.Apply.Internal: SuccessEpenthesis :: Bool -> RuleStatus
- Brassica.SoundChange.Apply.Internal: getChangedOutputs :: Log (Statement c d) -> Maybe (PWord, Bool)
+ Brassica.SoundChange.Apply.Internal: getChangedOutputs :: forall (c :: LexemeType -> Type) d. HighlightMode -> Log (Statement c d) -> Maybe (PWord, Bool)
- Brassica.SoundChange.Apply.Internal: getChangedReports :: Log (Statement c d) -> [(PWord, Bool)]
+ Brassica.SoundChange.Apply.Internal: getChangedReports :: forall (c :: LexemeType -> Type) d. HighlightMode -> Log (Statement c d) -> [(PWord, Bool)]
- Brassica.SoundChange.Expand: expand :: Categories -> CategorySpec a -> Either ExpandError (Expanded a)
+ Brassica.SoundChange.Expand: expand :: forall (a :: LexemeType). Categories -> CategorySpec a -> Either ExpandError (Expanded a)
- Brassica.SoundChange.Expand: expandSoundChanges :: SoundChanges CategorySpec Directive -> Either ExpandError (SoundChanges Expanded GraphemeList)
+ Brassica.SoundChange.Expand: expandSoundChanges :: SoundChanges CategorySpec Directive -> Either (Maybe Int, ExpandError) (SoundChanges Expanded GraphemeList)
- Brassica.SoundChange.Expand: lookup :: String -> Categories -> Maybe (Either (Expanded a) AutosegmentDef)
+ Brassica.SoundChange.Expand: lookup :: forall (a :: LexemeType). String -> Categories -> Maybe (Either (Expanded a) AutosegmentDef)
- Brassica.SoundChange.Expand: type Categories = Map String (Either (Expanded 'AnyPart) AutosegmentDef)
+ Brassica.SoundChange.Expand: type Categories = Map String Either Expanded 'AnyPart AutosegmentDef
- Brassica.SoundChange.Frontend.Internal: DifferentToInput :: HighlightMode
+ Brassica.SoundChange.Frontend.Internal: DifferentToInput :: HighlightMode -> HighlightMode
- Brassica.SoundChange.Frontend.Internal: parseTokeniseAndApplyRules :: (forall a b. (a -> b) -> [Component a] -> [Component b]) -> SoundChanges Expanded GraphemeList -> String -> InputLexiconFormat -> ApplicationMode -> Maybe [Component PWord] -> ApplicationOutput PWord (Statement Expanded GraphemeList)
+ Brassica.SoundChange.Frontend.Internal: parseTokeniseAndApplyRules :: (forall a b. () => (a -> b) -> [Component a] -> [Component b]) -> SoundChanges Expanded GraphemeList -> String -> InputLexiconFormat -> ApplicationMode -> Maybe [Component PWord] -> ApplicationOutput PWord (Statement Expanded GraphemeList)
- Brassica.SoundChange.Tokenise: findFirstCategoriesDecl :: SoundChanges c GraphemeList -> [String]
+ Brassica.SoundChange.Tokenise: findFirstCategoriesDecl :: forall (c :: LexemeType -> Type). SoundChanges c GraphemeList -> [String]
- Brassica.SoundChange.Tokenise: withFirstCategoriesDecl :: ([String] -> t) -> SoundChanges c GraphemeList -> t
+ Brassica.SoundChange.Tokenise: withFirstCategoriesDecl :: forall t (c :: LexemeType -> Type). ([String] -> t) -> SoundChanges c GraphemeList -> t
- Brassica.SoundChange.Types: Categories :: Bool -> Bool -> [CategoryDefinition] -> Directive
+ Brassica.SoundChange.Types: Categories :: Int -> Bool -> Bool -> [CategoryDefinition] -> Directive
- Brassica.SoundChange.Types: CategorySpec :: [(CategoryModification, CategoryElement CategorySpec a)] -> CategorySpec a
+ Brassica.SoundChange.Types: CategorySpec :: [(CategoryModification, CategoryElement CategorySpec a)] -> CategorySpec (a :: LexemeType)
- Brassica.SoundChange.Types: DeclS :: decl -> Statement c decl
+ Brassica.SoundChange.Types: DeclS :: decl -> Statement (c :: LexemeType -> Type) decl
- Brassica.SoundChange.Types: Filter :: String -> [Lexeme c 'Matched] -> Filter c
+ Brassica.SoundChange.Types: Filter :: String -> Int -> [Lexeme c 'Matched] -> Filter (c :: LexemeType -> Type)
- Brassica.SoundChange.Types: FilterS :: Filter c -> Statement c decl
+ Brassica.SoundChange.Types: FilterS :: Filter c -> Statement (c :: LexemeType -> Type) decl
- Brassica.SoundChange.Types: Flags :: Bool -> Direction -> Bool -> Sporadicity -> Bool -> Flags
+ Brassica.SoundChange.Types: Flags :: Bool -> Bool -> Direction -> Bool -> Sporadicity -> Bool -> Maybe String -> Flags
- Brassica.SoundChange.Types: FromElements :: [CategoryElement Expanded a] -> Expanded a
+ Brassica.SoundChange.Types: FromElements :: [CategoryElement Expanded a] -> Expanded (a :: LexemeType)
- Brassica.SoundChange.Types: MustInline :: String -> CategorySpec a
+ Brassica.SoundChange.Types: MustInline :: String -> CategorySpec (a :: LexemeType)
- Brassica.SoundChange.Types: ReportS :: Statement c decl
+ Brassica.SoundChange.Types: ReportS :: Statement (c :: LexemeType -> Type) decl
- Brassica.SoundChange.Types: Rule :: [Lexeme c 'Matched] -> [Lexeme c 'Replacement] -> [Environment c] -> Maybe (Environment c) -> Flags -> String -> Rule c
+ Brassica.SoundChange.Types: Rule :: [Lexeme c 'Matched] -> [Lexeme c 'Replacement] -> [Environment c] -> [Environment c] -> Flags -> String -> Int -> Rule (c :: LexemeType -> Type)
- Brassica.SoundChange.Types: RuleS :: Rule c -> Statement c decl
+ Brassica.SoundChange.Types: RuleS :: Rule c -> Statement (c :: LexemeType -> Type) decl
- Brassica.SoundChange.Types: [Autosegment] :: Grapheme -> [[(Grapheme, Bool)]] -> [Grapheme] -> Lexeme category a
+ Brassica.SoundChange.Types: [Autosegment] :: forall (category :: LexemeType -> Type) (a :: LexemeType). Grapheme -> [[(Grapheme, Bool)]] -> [Grapheme] -> Lexeme category a
- Brassica.SoundChange.Types: [Backreference] :: Either String Int -> category a -> Lexeme category a
+ Brassica.SoundChange.Types: [Backreference] :: forall (category :: LexemeType -> Type) (a :: LexemeType). Either String Int -> category a -> Lexeme category a
- Brassica.SoundChange.Types: [Category] :: category a -> Lexeme category a
+ Brassica.SoundChange.Types: [Category] :: forall (category :: LexemeType -> Type) (a :: LexemeType). category a -> Lexeme category a
- Brassica.SoundChange.Types: [Discard] :: Lexeme category 'Replacement
+ Brassica.SoundChange.Types: [Discard] :: forall (category :: LexemeType -> Type). Lexeme category 'Replacement
- Brassica.SoundChange.Types: [Feature] :: Bool -> String -> Maybe String -> [[Grapheme]] -> Lexeme category a -> Lexeme category a
+ Brassica.SoundChange.Types: [Feature] :: forall (category :: LexemeType -> Type) (a :: LexemeType). Bool -> String -> Maybe String -> [[Grapheme]] -> Lexeme category a -> Lexeme category a
- Brassica.SoundChange.Types: [Geminate] :: Lexeme category a
+ Brassica.SoundChange.Types: [Geminate] :: forall (category :: LexemeType -> Type) (a :: LexemeType). Lexeme category a
- Brassica.SoundChange.Types: [Grapheme] :: Grapheme -> Lexeme category a
+ Brassica.SoundChange.Types: [Grapheme] :: forall (category :: LexemeType -> Type) (a :: LexemeType). Grapheme -> Lexeme category a
- Brassica.SoundChange.Types: [GreedyCategory] :: category 'Matched -> Lexeme category 'Matched
+ Brassica.SoundChange.Types: [GreedyCategory] :: forall (category :: LexemeType -> Type). category 'Matched -> Lexeme category 'Matched
- Brassica.SoundChange.Types: [GreedyOptional] :: [Lexeme category 'Matched] -> Lexeme category 'Matched
+ Brassica.SoundChange.Types: [GreedyOptional] :: forall (category :: LexemeType -> Type). [Lexeme category 'Matched] -> Lexeme category 'Matched
- Brassica.SoundChange.Types: [Kleene] :: Lexeme category a -> Lexeme category a
+ Brassica.SoundChange.Types: [Kleene] :: forall (category :: LexemeType -> Type) (a :: LexemeType). Lexeme category a -> Lexeme category a
- Brassica.SoundChange.Types: [Metathesis] :: Lexeme category 'Replacement
+ Brassica.SoundChange.Types: [Metathesis] :: forall (category :: LexemeType -> Type). Lexeme category 'Replacement
- Brassica.SoundChange.Types: [Multiple] :: category 'Replacement -> Lexeme category 'Replacement
+ Brassica.SoundChange.Types: [Multiple] :: forall (category :: LexemeType -> Type). category 'Replacement -> Lexeme category 'Replacement
- Brassica.SoundChange.Types: [Optional] :: [Lexeme category a] -> Lexeme category a
+ Brassica.SoundChange.Types: [Optional] :: forall (category :: LexemeType -> Type) (a :: LexemeType). [Lexeme category a] -> Lexeme category a
- Brassica.SoundChange.Types: [Wildcard] :: Lexeme category a -> Lexeme category a
+ Brassica.SoundChange.Types: [Wildcard] :: forall (category :: LexemeType -> Type) (a :: LexemeType). Lexeme category a -> Lexeme category a
- Brassica.SoundChange.Types: [elements] :: Expanded a -> [CategoryElement Expanded a]
+ Brassica.SoundChange.Types: [elements] :: Expanded (a :: LexemeType) -> [CategoryElement Expanded a]
- Brassica.SoundChange.Types: [environment] :: Rule c -> [Environment c]
+ Brassica.SoundChange.Types: [environment] :: Rule (c :: LexemeType -> Type) -> [Environment c]
- Brassica.SoundChange.Types: [exception] :: Rule c -> Maybe (Environment c)
+ Brassica.SoundChange.Types: [exception] :: Rule (c :: LexemeType -> Type) -> [Environment c]
- Brassica.SoundChange.Types: [flags] :: Rule c -> Flags
+ Brassica.SoundChange.Types: [flags] :: Rule (c :: LexemeType -> Type) -> Flags
- Brassica.SoundChange.Types: [plaintext] :: Rule c -> String
+ Brassica.SoundChange.Types: [plaintext] :: Rule (c :: LexemeType -> Type) -> String
- Brassica.SoundChange.Types: [replacement] :: Rule c -> [Lexeme c 'Replacement]
+ Brassica.SoundChange.Types: [replacement] :: Rule (c :: LexemeType -> Type) -> [Lexeme c 'Replacement]
- Brassica.SoundChange.Types: [target] :: Rule c -> [Lexeme c 'Matched]
+ Brassica.SoundChange.Types: [target] :: Rule (c :: LexemeType -> Type) -> [Lexeme c 'Matched]
- Brassica.SoundChange.Types: data CategorySpec a
+ Brassica.SoundChange.Types: data CategorySpec (a :: LexemeType)
- Brassica.SoundChange.Types: data Filter c
+ Brassica.SoundChange.Types: data Filter (c :: LexemeType -> Type)
- Brassica.SoundChange.Types: data Lexeme category (a :: LexemeType)
+ Brassica.SoundChange.Types: data Lexeme (category :: LexemeType -> Type) (a :: LexemeType)
- Brassica.SoundChange.Types: data Rule c
+ Brassica.SoundChange.Types: data Rule (c :: LexemeType -> Type)
- Brassica.SoundChange.Types: data Statement c decl
+ Brassica.SoundChange.Types: data Statement (c :: LexemeType -> Type) decl
- Brassica.SoundChange.Types: generalise :: (c 'AnyPart -> c a) -> Lexeme c 'AnyPart -> Lexeme c a
+ Brassica.SoundChange.Types: generalise :: forall c (a :: LexemeType). (c 'AnyPart -> c a) -> Lexeme c 'AnyPart -> Lexeme c a
- Brassica.SoundChange.Types: generaliseExpanded :: Expanded 'AnyPart -> Expanded a
+ Brassica.SoundChange.Types: generaliseExpanded :: forall (a :: LexemeType). Expanded 'AnyPart -> Expanded a
- Brassica.SoundChange.Types: mapCategory :: (forall x. c x -> c' x) -> Lexeme c a -> Lexeme c' a
+ Brassica.SoundChange.Types: mapCategory :: forall c c' (a :: LexemeType). (forall (x :: LexemeType). () => c x -> c' x) -> Lexeme c a -> Lexeme c' a
- Brassica.SoundChange.Types: mapCategoryA :: Applicative t => (forall x. c x -> t (c' x)) -> Lexeme c a -> t (Lexeme c' a)
+ Brassica.SoundChange.Types: mapCategoryA :: forall t c c' (a :: LexemeType). Applicative t => (forall (x :: LexemeType). () => c x -> t (c' x)) -> Lexeme c a -> t (Lexeme c' a)
- Brassica.SoundChange.Types: newtype Expanded a
+ Brassica.SoundChange.Types: newtype Expanded (a :: LexemeType)
- Brassica.SoundChange.Types: plaintext' :: Statement c decl -> String
+ Brassica.SoundChange.Types: plaintext' :: forall (c :: LexemeType -> Type) decl. Statement c decl -> String
- Brassica.SoundChange.Types: type CategoryElement category a = [Lexeme category a]
+ Brassica.SoundChange.Types: type CategoryElement (category :: LexemeType -> Type) (a :: LexemeType) = [Lexeme category a]
- Brassica.SoundChange.Types: type Environment c = ([Lexeme c 'Matched], [Lexeme c 'Matched])
+ Brassica.SoundChange.Types: type Environment (c :: LexemeType -> Type) = ([Lexeme c 'Matched], [Lexeme c 'Matched])
- Brassica.SoundChange.Types: type SoundChanges c decl = [Statement c decl]
+ Brassica.SoundChange.Types: type SoundChanges (c :: LexemeType -> Type) decl = [Statement c decl]

Files

ChangeLog.md view
@@ -1,5 +1,62 @@ # Brassica changelog +## 1.1.0++### Behaviour++- Bugfixes:+  - The first element of categories is now treated consistently+      accross both category blocks and inline categories.+  - Combining optional elements with the Kleene star no longer causes an infinite loop+  - Highlighting ‘any rule applied’ with ‘input→output’ format now works correctly+- Improvements to paradigm builder:+  - Stem alternations are now supported with new `~n` syntax+  - Paradigm classes can be specified for individual words with new `#paradigm` syntax+  - Feature mappings and `when` conditions can now include multiple alternatives+  - Blank lines are now allowed in paradigm specifications+- Kleene star can now produce multiple results, no longer restricted to greedy matching (resolving https://github.com/bradrn/brassica/issues/7)+- Epenthesis rules with environments matching replacements no longer yield an infinite loop (resolving https://github.com/bradrn/brassica/issues/10)+- New option to highlight only words affected by specific rules, selected with new flag `-h`+- New flag `-pstart <name>` to create persistent rules, disabled with new directive `pstop <name>`+- Allow specifying multiple exceptions in a single rule+- New checkbox / command-line option to highlight unused rules in the rules editor+- ‘Input→output’ format and ‘Report rules applied’ both now show unchanged words explicitly+- Syntax errors are now highlighted in the rules editor+- In live mode, output box is greyed out rather than being interrupted with error messages+- On desktop and web, results are now computed asynchronously (i.e. without causing the UI to freeze)++### Code++- New flag `highlightSpecificRule` added to `Brassica.SoundChanges.Types.Flags`+- `Brassica.SoundChanges.Apply.getChangedOutputs` and `Brassica.SoundChanges.Apply.getChangedReports`+    now take an extra parameter of new type `Brassica.SoundChanges.Apply.HighlightMode`,+    as does the constructor `Brassica.SoundChange.Frontend.Internal.DifferentToInput`+- New function `Brassica.SoundChanges.Apply.rulesNotApplied`+- Method of reporting rules to frontend is now selected by new type `Brassica.SoundChange.Frontend.Internal.ReportMode`,+    with corresponding changes in `Brassica.SoundChange.Frontend.Internal.ApplicationMode`+    and `Brassica.SoundChange.Frontend.Internal.ApplicationOutput`+- Changed `Brassica.SoundChange.Types.Rule` to admit multiple exceptions+- New field for line number in `Brassica.SoundChange.Types.Rule`,+    `Brassica.SoundChange.Types.Filter`, `Brassica.SoundChanges.Types.Directive`+- Persistent rules:+  new flag `persistent` in `Brassica.SoundChange.Types.Flags`,+  and new directive `DisablePersistent` in same module+- Changes to paradigm builder:+  - New constructors `Stem` and `Class` in `Brassica.Paradigm.Types`+  - New type `Brassica.Paradigm.Expr`, now used in `Statement` and `Feature` in the same module+  - Constructor `Brassica.Paradigm.Always` removed (it can be replaced by `All []`)+  - `Brassica.Paradigm.Feature` now takes a `String` rather than a `FeatureName`+  - `Brassica.Paradigm.applyParadigm` now takes a list of stems with paradigm specification+      rather than a single root+  - New function `Brassica.Paradigm.parseStems` to parse stems with paradigm specifications+- New field in constructor `Brassica.SoundChange.Apply.Internal.SuccessEpenthesis`+- Web communication protocol now uses JSON as with desktop:+  - `ToJSON` and `FromJSON` instances added for `Brassica.SoundChange.Tokenise.Component`+  - `Enum` instances removed from types in `Brassica.SoundChange.Frontend.Internal` as now unnecessary+  - JSON server implementation moved from CLI to `Brassica.SoundChange.Frontend.Internal`;+      CLI `Server` and web `BrassicaInterop` modules are both now just a thin wrapper over this module++ ## 1.0.0  ### Behaviour
LICENSE view
@@ -1,4 +1,4 @@-Copyright Brad Neimann (c) 2020-2024+Copyright Brad Neimann (c) 2020-2026  All rights reserved. 
brassica.cabal view
@@ -1,16 +1,16 @@ cabal-version:      2.0 name:               brassica-version:            1.0.0+version:            1.1.0 synopsis:           Featureful sound change applier description:   This is the Brassica library for the simulation of sound changes in historical linguistics and language construction.   .-  For an introduction to library usage, see the documentation of [Brassica.SoundChange](https://hackage.haskell.org/package/brassica-1.0.0/candidate/docs/Brassica-SoundChange.html).+  For an introduction to library usage, see the documentation of [Brassica.SoundChange](https://hackage.haskell.org/package/brassica-1.1.0/candidate/docs/Brassica-SoundChange.html).   .-  For further details on Brassica’s design and usage, please refer to the README below or at <https://github.com/bradrn/brassica/blob/v1.0.0#readme>.+  For further details on Brassica’s design and usage, please refer to the README below or at <https://github.com/bradrn/brassica/blob/v1.1.0#readme>.  category:           Linguistics-homepage:           https://github.com/bradrn/brassica#readme+homepage:           https://bradrn.com/brassica bug-reports:        https://github.com/bradrn/brassica/issues author:             Brad Neimann maintainer:         Brad Neimann@@ -46,13 +46,16 @@   ghc-options:      -Wall   build-depends:                    base >=4.7 && <5+                 , aeson >=2.2 && <2.4                  , containers >=0.6 && <0.7                  , deepseq >=1.4 && <1.6-                 , fast-myers-diff ==0.0.0-                 , megaparsec >=8.0 && <9.7+                 , fast-myers-diff >=0.0.0 && <0.1+                 , megaparsec >=8.0 && <9.9                  , mtl >=2.2 && <2.4-                 , parser-combinators >=1.2 && <1.3+                 , parallel >=3.2 && <3.4+                 , parser-combinators >=1.2 && <1.4                  , split >=0.2 && <0.3+                 , text >=1.2 && <2.3                  , transformers >=0.5 && <0.7                  , vector >=0.13 && <0.14 @@ -66,14 +69,13 @@   build-depends:                     base >=4.7 && <5                   , brassica-                  , aeson >=2.2 && <2.3+                  , aeson >=2.2 && <2.4                   , attoparsec-aeson ^>=2.2                   , bytestring >=0.10 && <0.13                   , conduit ^>=1.3                   , conduit-extra ^>=1.3                   , deepseq >=1.4 && <1.6-                  , optparse-applicative ^>=0.17.1 || ^>=0.18-                  , parallel ^>= 3.2+                  , optparse-applicative >=0.17.1 && <0.20                   , text >=1.2 && <2.3    default-language: Haskell2010@@ -87,7 +89,7 @@                   , brassica                   , bytestring >=0.10 && <0.13                   , conduit ^>=1.3-                  , optparse-applicative ^>=0.17.1 || ^>=0.18+                  , optparse-applicative >=0.17.1 && <0.20                   , text >=1.2 && <2.3    default-language: Haskell2010@@ -101,8 +103,8 @@                     base >=4.7 && <5                   , brassica                   , criterion >=1.5 && <1.7-                  , file-embed >=0.0.15 && <0.0.16-                  , parallel ^>= 3.2+                  , file-embed >=0.0.15 && <0.0.17+                  , parallel >=3.2 && <3.4                   , text >=1.2 && <2.3    default-language: Haskell2010@@ -127,7 +129,7 @@   build-depends:                     base >=4.7 && <5                   , brassica-                  , tasty ^>=1.4+                  , tasty >=1.4 && <1.6                   , tasty-golden ^>=2.3                   , bytestring                   , text >=1.2 && <2.3@@ -145,10 +147,10 @@   build-depends:                     base >=4.7 && <5                   , brassica-                  , tasty ^>=1.4+                  , tasty >=1.4 && <1.6                   , bytestring                   , text >=1.2 && <2.3-                  , pandoc >=3.3 && <3.4+                  , pandoc >=3.3 && <3.13                   , pandoc-types >=1.23 && <1.24    default-language: Haskell2010
cli-pb/Main.hs view
@@ -34,7 +34,7 @@                 .| encodeUtf8C                 .| outC   where-    opts = info (args <**> helper <**> simpleVersioner "v1.0.0") fullDesc+    opts = info (args <**> helper <**> simpleVersioner "v1.1.0") fullDesc      args = Options         <$> asum@@ -58,7 +58,7 @@      processRoot :: Bool -> Paradigm -> Text -> Text     processRoot nestedOutput p r =-        let output = applyParadigm p $ unpack r+        let output = uncurry (applyParadigm p) $ parseStems $ unpack r         in if nestedOutput             then addSpaces output $ pack $ formatNested id output             else T.unlines $ pack <$> toList output
cli/Main.hs view
@@ -29,7 +29,7 @@                 putStrLn $ errorBundlePretty err             Right scs ->                 case expandSoundChanges scs of-                    Left err -> putStrLn $ case err of+                    Left (_, err) -> putStrLn $ case err of                         (NotFound s) -> "Could not find category: " ++ s                         InvalidBaseValue -> "Invalid value used as base grapheme in feature definition"                         InvalidDerivedValue -> "Invalid value used as derived grapheme in autosegment"@@ -39,11 +39,11 @@                         withSinkFileIf outWordsFile $ \outC ->                         runConduit $                             inC-                            .| processWords (incrFor wordsFormat) rules' wordsFormat outMode+                            .| processWords (incrFor wordsFormat outMode) rules' wordsFormat outMode                             .| outC    where-    opts = info (args <**> helper <**> simpleVersioner "v1.0.0") fullDesc+    opts = info (args <**> helper <**> simpleVersioner "v1.1.0") fullDesc      args = batchArgs <|> serverArgs     serverArgs = flag' Server (long "server" <> help "Run server (for internal use only)")@@ -61,16 +61,20 @@                 (long "mdf-alt" <> help "Parse input words in MDF format (alternate hierarchy)")             ]         <*> (asum-                [ flag' (const ReportRulesApplied)+                [ flag' (const $ ReportRules ReportApplied)                     (long "report" <> help "Report rules applied rather than outputting words")+                , flag' (const $ ReportRules ReportNotApplied)+                    (long "report-not" <> help "Report rules which didn't apply rather than outputting words")                 , flag' (ApplyRules NoHighlight MDFOutput)                     (long "mdf-out" <> help "With --mdf, output MDF dictionary")                 , flag' (ApplyRules NoHighlight MDFOutputWithEtymons)                     (long "etymons" <> help "With --mdf, output MDF dictionary with etymologies")                 , flag' (ApplyRules NoHighlight WordsWithProtoOutput)                     (long "show-input" <> help "Output an input→output wordlist")-                , flag' (ApplyRules DifferentToInput WordsOnlyOutput)+                , flag' (ApplyRules (DifferentToInput AllChanged) WordsOnlyOutput)                     (long "show-changed" <> help "Add [+] after all words different to input")+                , flag' (ApplyRules (DifferentToInput SpecificRule) WordsOnlyOutput)+                    (long "show-changed-h" <> help "Add [+] after all words affected by rules with -h")                 , flag                     (ApplyRules NoHighlight WordsOnlyOutput)                     (ApplyRules NoHighlight WordsOnlyOutput)@@ -83,8 +87,9 @@         <*> optional (strOption             (long "out" <> short 'o' <> help "File to which output words should be written (if not specified will write to stdout)")) -    incrFor Raw = True-    incrFor (MDF _) = False+    incrFor (MDF _) _ = False+    incrFor Raw (ReportRules ReportNotApplied) = False+    incrFor Raw _ = True      -- duplicated in paradigm builder CLI     withSourceFileIf :: Maybe FilePath -> (ConduitM i B.ByteString IO () -> IO a) -> IO a@@ -130,6 +135,7 @@     processApplicationOutput :: ApplicationOutput PWord (Statement Expanded GraphemeList) -> Either ParseException Text     processApplicationOutput (HighlightedWords cs) = Right $ ensureNewline $ pack $ detokeniseWords' highlight cs     processApplicationOutput (AppliedRulesTable is) = Right $ pack $ unlines $ reportAsText plaintext' <$> is+    processApplicationOutput (NotAppliedRulesList is) = Right $ pack $ unlines $ plaintext <$> is     processApplicationOutput (ParseError e) = Left $ ParseException $ errorBundlePretty e      ensureNewline t = case unsnoc t of
cli/Server.hs view
@@ -1,83 +1,18 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TemplateHaskell #-}-{-# OPTIONS_GHC -Wno-orphans #-}-{-# OPTIONS_GHC -Wno-incomplete-record-updates #-} -module Server (serve, parFmap) where+module Server (serve) where +import Data.Aeson (Result(..), encode, fromJSON) import Conduit (runConduit, (.|), stdinC, stdoutC, mapMC)-import Control.DeepSeq (force, NFData)+import Control.DeepSeq (force) import Control.Exception (evaluate)-import Control.Parallel.Strategies (withStrategy, parTraversable, rseq)-import Data.Aeson (Result(..), encode, FromJSON(..), ToJSON(..), Value (..), fromJSON) import Data.Aeson.Parser (json')-import Data.Aeson.TH (deriveJSON, defaultOptions, defaultTaggedObject, constructorTagModifier, sumEncoding, tagFieldName)-import Data.Aeson.Types (prependFailure, typeMismatch) import Data.ByteString (toStrict) import Data.Conduit.Attoparsec (conduitParser)-import Data.Foldable (toList)-import Data.Text (unpack)-import GHC.Generics (Generic) import System.IO (hSetBuffering, stdin, stdout, BufferMode(NoBuffering)) import System.Timeout -import Brassica.SoundChange-import Brassica.SoundChange.Frontend.Internal-import Brassica.Paradigm (applyParadigm, parseParadigm, formatNested, ResultsTree (..))--data Request-    = ReqRules-        { changes :: String-        , input :: String-        , report :: Bool-        , inFmt :: InputLexiconFormat-        , hlMode :: HighlightMode-        , outMode :: OutputMode-        , prev :: Maybe [Component PWord]-        , sep :: String-        }-    | ReqParadigm-        { pText :: String-        , input :: String-        , separateLines :: Bool-        }-    deriving (Show)--data Response-    = RespRules-        { prev :: Maybe [Component PWord]-        , output :: String-        }-    | RespParadigm-        { output :: String-        }-    | RespError String-    deriving (Show, Generic, NFData)--instance ToJSON InputLexiconFormat where-    toJSON Raw = "Raw"-    toJSON (MDF Standard) = "MDFStandard"-    toJSON (MDF Alternate) = "MDFAlternate"--instance FromJSON InputLexiconFormat where-    parseJSON (String "Raw") = pure Raw-    parseJSON (String "MDFStandard") = pure $ MDF Standard-    parseJSON (String "MDFAlternate") = pure $ MDF Alternate-    parseJSON (String s) = fail $ "Unknown InputLexiconFormat: " ++ unpack s-    parseJSON invalid = prependFailure "parsing InputLexiconFormat failed: " $-        typeMismatch "String" invalid--$(deriveJSON defaultOptions ''Component)-$(deriveJSON defaultOptions ''HighlightMode)-$(deriveJSON defaultOptions ''OutputMode)--$(deriveJSON defaultOptions{constructorTagModifier=drop 3, sumEncoding=defaultTaggedObject{tagFieldName="method"}} ''Request)-$(deriveJSON defaultOptions{constructorTagModifier=drop 4, sumEncoding=defaultTaggedObject{tagFieldName="method"}} ''Response)+import Brassica.SoundChange.Frontend.Internal (dispatch, Response(RespError), reqTimeout)  serve :: IO () serve = do@@ -91,71 +26,12 @@   where     action req' = fmap ((<> "\ETB") . toStrict . encode) $         case fromJSON req' of-            Error e -> pure $ RespError e+            Error e -> pure $ RespError [] e             Success req -> do                 result <--                    timeout 5000000 $  -- 5 s+                    timeout (reqTimeout req) $                     evaluate $ force $                     dispatch req                 pure $ case result of-                    Nothing -> RespError "&lt;timeout&gt;"+                    Nothing -> RespError [] "&lt;timeout&gt;"                     Just resp -> resp--dispatch :: Request -> Response-dispatch r@(ReqRules{}) = parseTokeniseAndApplyRulesWrapper r-dispatch r@(ReqParadigm{}) = parseAndBuildParadigmWrapper r--parseTokeniseAndApplyRulesWrapper-    :: Request-    -> Response-parseTokeniseAndApplyRulesWrapper ReqRules{..} =-    let mode =-            if report-            then ReportRulesApplied-            else ApplyRules hlMode outMode sep-    in case parseSoundChanges changes of-        Left e -> RespError $ "<pre>" ++ errorBundlePretty e ++ "</pre>"-        Right statements ->-            case expandSoundChanges statements of-                Left err -> RespError $ ("<pre>"++) $ (++"</pre>") $ case err of-                    (NotFound s) -> "Could not find category: " ++ s-                    InvalidBaseValue -> "Invalid value used as base grapheme in feature definition"-                    InvalidDerivedValue -> "Invalid value used as derived grapheme in autosegment"-                    MismatchedLengths -> "Mismatched lengths in feature definition"-                Right statements' ->-                    let result' = parseTokeniseAndApplyRules parFmap statements' input inFmt mode prev-                    in case result' of-                        ParseError e -> RespError $-                            "<pre>" ++ errorBundlePretty e ++ "</pre>"-                        HighlightedWords result -> RespRules-                            (Just $ (fmap.fmap) fst result)-                            (escape $ detokeniseWords' highlightWord result)-                        AppliedRulesTable items -> RespRules Nothing $-                            concatMap (surroundTable . reportAsHtmlRows plaintext') items-  where-    highlightWord (s, False) = concatWithBoundary s-    highlightWord (s, True) = "<b>" ++ concatWithBoundary s ++ "</b>"--    surroundTable :: String -> String-    surroundTable s = "<table>" ++ s ++ "</table>"-parseTokeniseAndApplyRulesWrapper _ = error "parseTokeniseAndApplyRulesWrapper: unexpected request!"--parFmap :: (a -> b) -> [Component a] -> [Component b]-parFmap f = withStrategy (parTraversable rseq) . fmap (fmap f)--parseAndBuildParadigmWrapper :: Request -> Response-parseAndBuildParadigmWrapper ReqParadigm{..} =-    case parseParadigm pText of-        Left e -> RespError $ "<pre>" ++ errorBundlePretty e ++ "</pre>"-        Right p -> RespParadigm $ escape $-            (if separateLines-                then unlines . toList-                else formatNested id)-            $ Node $ applyParadigm p <$> lines input-parseAndBuildParadigmWrapper _ = error "parseAndBuildParadigmWrapper: unexpected request!"--escape :: String -> String-escape = concatMap $ \case-    '\n' -> "<br/>"-    -- '\t' -> "&#9;"  -- this doesn't seem to do anything - keeping it here in case I eventually figure out how to do tabs in Qt-    c    -> pure c
src/Brassica/Paradigm.hs view
@@ -14,12 +14,14 @@        , Grammeme(..)        , AbstractGrammeme(..)        , Condition(..)+       , Expr(..)        , Feature(..)        , FeatureName(..)        , Statement(..)        , Paradigm        -- * Parsing and pretty-printing        , parseParadigm+       , parseStems        , formatNested        -- * Paradigm application        , applyParadigm
src/Brassica/Paradigm/Apply.hs view
@@ -12,7 +12,7 @@ import Brassica.Paradigm.Types  import Data.Functor ((<&>))-import Data.List (sortOn, foldl', intercalate)+import Data.List (sortOn, foldl', intercalate, nub) import Data.Maybe (mapMaybe) import Data.Ord (Down(Down)) @@ -21,8 +21,11 @@ data ResultsTree a = Node [ResultsTree a] | Result a     deriving (Show, Functor, Foldable) -addLevel :: (a -> [a]) -> ResultsTree a -> ResultsTree a-addLevel f (Result r) = Node $ Result <$> f r+addLevel :: (a -> [b]) -> ResultsTree a -> ResultsTree b+addLevel f (Result r) =+    case f r of+        [r'] -> Result r'+        r' -> Node $ Result <$> r' addLevel f (Node rs) = Node $ addLevel f <$> rs  -- | Depth of a 'ResultsTree': i.e. how many levels of 'Node's it@@ -50,11 +53,11 @@  -- | Apply the given 'Paradigm' to a root, to produce all possible -- derived forms.-applyParadigm :: Paradigm -> String -> ResultsTree String-applyParadigm p w =+applyParadigm :: Paradigm -> Maybe String -> [String] -> ResultsTree String+applyParadigm p pclass ws =     let fs = mapMaybe getFeature p         ms = mapMaybe getMapping p-    in applyTo w . expand ms <$> combinations fs+    in addLevel (applyTo ws . expand ms) $ combinations pclass fs   where     getFeature (NewFeature f) = Just f     getFeature _ = Nothing@@ -62,17 +65,25 @@     getMapping (NewMapping k v) = Just (k,v)     getMapping _ = Nothing -combinations :: [Feature] -> ResultsTree [Grammeme]-combinations =-    (fmap.fmap) snd . foldl' go (Result [])+evalExpr :: (a -> Bool) -> Expr a -> Bool+evalExpr p (Any xs) = any (evalExpr p) xs+evalExpr p (All xs) = all (evalExpr p) xs+evalExpr p (Pred a) = p a++combinations :: Maybe String -> [Feature] -> ResultsTree [Grammeme]+combinations pclass =+    let initGrammemes = case pclass of+            Just p -> [(Just Class, Abstract (AbstractGrammeme p))]+            _ -> []+    in (fmap.fmap) snd . foldl' go (Result initGrammemes)   where     addFeature         :: Feature         -> [(Maybe FeatureName, Grammeme)]         -> [[(Maybe FeatureName, Grammeme)]]     addFeature (Feature c n gs) acc-        | satisfied (flip lookup acc . Just) c-        = gs <&> \g -> (n, g):acc+        | evalExpr (satisfied (flip lookup acc . Just)) c+        = gs <&> \g -> (FeatureName <$> n, g):acc         | otherwise         = [acc] @@ -85,7 +96,6 @@         :: (FeatureName -> Maybe Grammeme)         -> Condition         -> Bool-    satisfied _ Always = True     satisfied l (Is n g) = case l n of         Just g' -> g == g'         Nothing -> False@@ -93,7 +103,7 @@         Just g' -> g /= g'         Nothing -> True -expand :: [([AbstractGrammeme], Affix)] -> [Grammeme] -> [Process]+expand :: [(Expr AbstractGrammeme, Affix)] -> [Grammeme] -> [Process] expand ms = concat . ((++) <$> concretes <*> (replace . filterAbstract))   where     concretes :: [Grammeme] -> [Affix]@@ -109,20 +119,25 @@     replace :: [AbstractGrammeme] -> [Affix]     replace gs = do         (condition, replacement) <- ms-        if condition `subsetOf` gs+        if evalExpr (`elem` gs) condition            then return replacement            else [] -    xs `subsetOf` ys = all (`elem` ys) xs--applyTo :: String -> [Process] -> String-applyTo w is =+applyTo :: [String] -> [Process] -> [String]+applyTo ws is =     let ps = concatMap snd $ sortOn (Down . fst) $ mapMaybe getPrefix is         ss = concatMap snd $ sortOn         fst  $ mapMaybe getSuffix is-    in ps ++ w ++ ss+        stems = case nub (mapMaybe getStem is) of+            [] | [_] <- ws -> ws+            [s] | s >= 1 && s <= length ws -> [ws !! (s-1)]+            s -> [show s]+    in stems <&> \stem -> ps ++ stem ++ ss   where     getPrefix (Prefix s i) = Just (s,i)     getPrefix _ = Nothing      getSuffix (Suffix s i) = Just (s,i)     getSuffix _ = Nothing++    getStem (Stem i) = Just i+    getStem _ = Nothing
src/Brassica/Paradigm/Parse.hs view
@@ -1,5 +1,8 @@-module Brassica.Paradigm.Parse (parseParadigm) where
+{-# LANGUAGE LambdaCase #-}
+module Brassica.Paradigm.Parse (parseParadigm, parseStems) where
 
+import Control.Category ((>>>))
+import Data.Maybe (fromMaybe)
 import Control.Monad (void)
 import Data.Char (isSpace)
 import Data.Void (Void)
@@ -9,7 +12,6 @@ import qualified Text.Megaparsec.Char.Lexer as L
 
 import Brassica.Paradigm.Types
-import Data.Maybe (fromMaybe)
 
 type Parser = Parsec Void String
 
@@ -21,6 +23,9 @@ sc :: Parser ()
 sc = L.space space1' (L.skipLineComment "*") empty
 
+eolf :: Parser ()
+eolf = void (some eol) <|> eof
+
 lexeme :: Parser a -> Parser a
 lexeme = L.lexeme sc
 
@@ -39,7 +44,9 @@ morphValue = lexeme (takeWhile1P (Just "letter") $ (&&) <$> (not.isSpace) <*> (/=')'))
 
 process :: Parser Process
-process = slot <* char '.' <*> morphValue
+process =
+    Stem <$> lexeme (char '~' *> L.decimal)
+    <|> (try (slot <* char '.') <*> morphValue)
 
 oneOrMany :: Parser p -> Parser [p]
 oneOrMany p = between (symbol "(") (symbol ")") (many p) <|> fmap pure p
@@ -52,31 +59,39 @@     Concrete <$> affix
     <|> Abstract . AbstractGrammeme <$> name
 
-condition :: Parser Condition
+parseExpr :: Parser a -> Parser (Expr a)
+parseExpr p = Any <$> ((All <$> some atom) `sepBy1` symbol "/")
+  where
+    atom = (Pred <$> p)
+        <|> between (symbol "(") (symbol ")") (parseExpr p)
+
+condition :: Parser (Expr Condition)
 condition = do
     _ <- symbol "when"
-    between (symbol "(") (symbol ")") $
-        try (Is . FeatureName <$> name <* symbol "is" <*> grammeme)
-        <|> Not . FeatureName <$> name <* symbol "not" <*> grammeme
+    parseExpr $ between (symbol "(") (symbol ")") $
+        try (Is <$> featureName <* symbol "is" <*> grammeme)
+        <|> Not <$> featureName <* symbol "not" <*> grammeme
+  where
+    featureName = (Class <$ lexeme (char '#')) <|> (FeatureName <$> name)
 
 feature :: Parser Feature
 feature = do
-    c <- fromMaybe Always <$> optional condition
+    c <- fromMaybe (All []) <$> optional condition
     globalSlot <- optional $ try $ slot <* space1'
     case globalSlot of
         Nothing -> do
             n <- optional $ try $ name <* symbol "="
             gs <- some grammeme
-            _ <- optional eol
-            return $ Feature c (FeatureName <$> n) gs
+            eolf
+            return $ Feature c n gs
         Just globalSlot' -> do
             n <- optional $ try $ name <* symbol "="
             gs <- some $ oneOrMany $ process <|> (globalSlot' <$> morphValue)
-            _ <- optional eol
-            return $ Feature c (FeatureName <$> n) (Concrete <$> gs)
+            eolf
+            return $ Feature c n (Concrete <$> gs)
 
-mapping :: Parser ([AbstractGrammeme], Affix)
-mapping = (,) <$> manyTill (AbstractGrammeme <$> name) (symbol ">") <*> affix <* optional eol
+mapping :: Parser (Expr AbstractGrammeme, Affix)
+mapping = (,) <$> parseExpr (AbstractGrammeme <$> name) <* symbol ">" <*> some process <* eolf
 
 statement :: Parser Statement
 statement = sc *>
@@ -86,6 +101,11 @@ -- | Parse a 'String' in Brassica paradigm syntax into a 'Paradigm'.
 -- Returns 'Left' if the input string is malformed.
 --
--- For details on the syntax, refer to <https://github.com/bradrn/brassica/blob/v1.0.0/docs/Paradigm-Builder.md>.
+-- For details on the syntax, refer to <https://github.com/bradrn/brassica/blob/v1.1.0/docs/Paradigm-Builder.md>.
 parseParadigm :: String -> Either (ParseErrorBundle String Void) Paradigm
 parseParadigm = runParser (many statement) ""
+
+parseStems :: String -> (Maybe String, [String])
+parseStems = words >>> \case
+    ('#':p):ws -> (Just p, ws)
+    ws -> (Nothing, ws)
src/Brassica/Paradigm/Types.hs view
@@ -5,12 +5,11 @@  import Data.String (IsString) --- | Represents a single morphophonological process: currently, either--- prefixation or suffixation of a 'String'. The 'Int' gives the--- distance of the affix from the root.+-- | Represents a single morphophonological process. data Process-    = Prefix Int String-    | Suffix Int String+    = Prefix Int String  -- ^ Prefixation at some distance from the root+    | Suffix Int String  -- ^ Suffixation at some distance from the root+    | Stem Int           -- ^ Select stem by number     deriving (Show, Eq)  -- | A single affix (using the term in a wide sense) can be thought of@@ -37,9 +36,7 @@ -- | A condition which must be satisfied for a 'Feature' to be -- included in a word. data Condition-    = Always-    -- ^ Condition which is always satisfied-    | Is FeatureName Grammeme+    = Is FeatureName Grammeme     -- ^ Satisfied when the specified feature has been assigned to the     -- specified 'Grammeme'     | Not FeatureName Grammeme@@ -47,22 +44,26 @@     -- to the specified 'Grammeme'     deriving (Show, Eq) +-- | An expression built up from 'Pred'icates, combined with logical+-- and ('All') or logical or ('Any').+data Expr a = Any [Expr a] | All [Expr a] | Pred a+    deriving (Show, Eq)+ -- | A grammatical feature, which may be realised by one of a -- selection of 'Grammeme's. A feature may be given a descriptive -- name, as well as a condition which must be satisfied for the -- 'Feature' to be included in a word.-data Feature = Feature Condition (Maybe FeatureName) [Grammeme]+data Feature = Feature (Expr Condition) (Maybe String) [Grammeme]     deriving (Show, Eq) --- | Name to identify a specific 'Feature'.-newtype FeatureName = FeatureName String+-- | Name to identify a specific 'Feature', or the paradigm 'Class'.+data FeatureName = FeatureName String | Class     deriving stock (Show, Eq)-    deriving newtype (IsString)  -- | Each statement in a paradigm description specifies either a new -- 'Feature', or a new mapping from a set of abstract grammemes to -- their realisation.-data Statement = NewFeature Feature | NewMapping [AbstractGrammeme] Affix+data Statement = NewFeature Feature | NewMapping (Expr AbstractGrammeme) Affix     deriving (Show, Eq)  -- | A paradigm is specified as a list of 'Statement's. The order is
src/Brassica/SoundChange.hs view
@@ -6,7 +6,7 @@ --
 -- The modules below provide Brassica’s support for sound changes.
 -- For further details on their syntax and processing, refer to the
--- [reference guide](https://github.com/bradrn/brassica/blob/v1.0.0/docs/Reference.md),
+-- [reference guide](https://github.com/bradrn/brassica/blob/v1.1.0/docs/Reference.md),
 -- and the documentation of individual modules.
 --
 -- In brief, a sound changes file passes through the following phases:
src/Brassica/SoundChange/Apply.hs view
@@ -22,9 +22,11 @@        , applyChanges        , Log(..)        , LogItem(..)+       , rulesNotApplied        -- * Reporting results        , getOutput        , getReports+       , HighlightMode(..)        , getChangedOutputs        , getChangedReports        , reportAsText
src/Brassica/SoundChange/Apply/Internal.hs view
@@ -85,8 +85,10 @@        , applyChanges
        , getOutput
        , getReports
+       , HighlightMode(..)
        , getChangedOutputs
        , getChangedReports
+       , rulesNotApplied
        ) where
 
 import Control.Applicative ((<|>))
@@ -323,12 +325,19 @@     -> [(MatchOutput, MultiZipper t Grapheme)]
 matchKleene origOut = go 0 origOut
   where
-    go !n out prev l mz = case match out prev l mz of
-        [] -> [
-            ( insertAtKleene (length $ matchedKleenes origOut) n out
-            , mz
-            ) ]
-        r -> r >>= \(out', mz') -> go (n+1) out' prev l mz'
+    go !n out prev l mz =
+        ( insertAtKleene (length $ matchedKleenes origOut) n out
+        , mz
+        ) : case match out prev l mz of
+        [] -> []
+        r -> r >>= \case
+            (out', mz')
+                | curPos mz == curPos mz' ->
+                    -- cursor didn't advance, avoid infinite loop!
+                    [( insertAtKleene (length $ matchedKleenes origOut) n out
+                     , mz
+                    )]
+                | otherwise -> go (n+1) out' prev l mz'
 
 matchWildcard
     :: MatchOutput
@@ -634,7 +643,9 @@ -- | Status of a rule application at a single location.
 data RuleStatus
     = SuccessNormal      -- ^ Rule was successful, with no need for special handling
-    | SuccessEpenthesis  -- ^ Rule was successful, but cursor was not advanced (need to avoid infinite loop)
+    | SuccessEpenthesis Bool
+    -- ^ Rule was successful, but cursor was not advanced (need to avoid infinite loop);
+    -- also report whether any graphemes were matched before the replacement
     | Failure            -- ^ Rule failed
     deriving (Eq, Show)
 
@@ -650,12 +661,13 @@         case result of
             Just out -> do
                 exs <- case exception of
-                    Nothing -> pure []
-                    Just ex -> gets $ join . toList .
-                        extend' (exceptionAppliesAtPoint target ex)
+                    [] -> pure []
+                    exEnvs -> gets $ \mz ->
+                        join . toList . flip extend' mz . exceptionAppliesAtPoint target =<< exEnvs
                 originalWord <- get
                 let pMay = locationOf TargetStart originalWord
                     pMay' = locationOf PrevEnd originalWord
+                    noneMatchedBeforeReplacement = pMay == locationOf AppStart originalWord
                 case pMay of
                     Nothing -> error "applyOnce: start of target was not tagged"
                     Just p
@@ -676,10 +688,8 @@                         -- but not TargetStart, so restore its old position
                         modifyMay $ tagAt TargetStart p
                         return $
-                            -- An epenthesis rule will cause an infinite loop
-                            -- if it matched no graphemes before the replacement
-                            if null (matchedGraphemes out) && null (fst env)
-                                then SuccessEpenthesis
+                            if null (matchedGraphemes out)
+                                then SuccessEpenthesis noneMatchedBeforeReplacement
                                 else SuccessNormal
             Nothing -> modifyMay (seek AppStart) >> go envs
 
@@ -694,8 +704,13 @@     resetTags <=< case status of
         SuccessNormal ->
             seek (if nonOverlappingTarget then TargetEnd else TargetStart)
-        SuccessEpenthesis ->
-            -- need to move forward if applying an epenthesis rule to avoid an infinite loop
+        SuccessEpenthesis False ->
+            -- treat as 'nonOverlappingTarget' to avoid possible infinite loops
+            -- (when the replacement would also match the first part of the target)
+            seek TargetEnd
+        SuccessEpenthesis True ->
+            -- matched no graphemes before replacement, so environment still matches:
+            -- move forward to avoid infinite loop
             seek TargetEnd >=> fwd
         Failure -> seek AppStart >=> fwd
   where
@@ -733,6 +748,7 @@             , exception = reverseEnv <$> exception r
             , flags = flags r
             , plaintext = plaintext r
+            , loc = loc r
             }
 
     reverseEnv (e1, e2) = (reverse e2, reverse e1)
@@ -747,7 +763,7 @@ 
 -- | Check if a 'MultiZipper' matches a 'Filter'.
 filterMatches :: Filter Expanded -> MultiZipper RuleTag Grapheme -> Bool
-filterMatches (Filter _ ls) = go . toBeginning
+filterMatches (Filter _ _ ls) = go . toBeginning
   where
     go mz =
         let mzs = matchMany initialOutput Nothing ls mz
@@ -805,6 +821,8 @@     = ActionApplied r (Maybe PWord)
     -- ^ The word was modified: gives the output word, or 'Nothing' if
     -- the wordwas deleted
+    | ActionNotApplied r PWord
+    -- ^ The word was not modified by the given rule
     | ReportWord PWord
     -- ^ Corresponds to 'ReportS', giving the intermediate form to report
     deriving (Show, Functor, Generic, NFData)
@@ -813,8 +831,13 @@ -- action (ActionApplied r _ _) = Just r
 -- action (ReportWord _) = Nothing
 
+noApplication :: LogItem r -> Bool
+noApplication (ActionNotApplied _ _) = True
+noApplication _ = False
+
 logOutput :: LogItem r -> Maybe PWord
 logOutput (ActionApplied _ o) = o
+logOutput (ActionNotApplied _ o) = Just o
 logOutput (ReportWord o) = Just o
 
 -- | Logs the evolution of a word as it undergoes sound changes and
@@ -844,14 +867,20 @@ reportAsHtmlRows
     :: (r -> String)  -- ^ Specifies how to pretty-print actions as text
     -> Log r -> String
-reportAsHtmlRows render item = go (concatWithBoundary $ inputWord item) (derivations item)
+reportAsHtmlRows render item
+    | all noApplication (derivations item) =
+        "<tr><td>" ++ input ++ "</td><td></td><td></td><td>(no change)</td>"
+    | otherwise = go input (derivations item)
   where
+    input = concatWithBoundary $ inputWord item
+
     go _ [] = ""
     go cell1 (ActionApplied action output : ds) =
         ("<tr><td>" ++ cell1 ++ "</td><td>&rarr;</td><td>"
          ++ maybe "<i>deleted</i>" concatWithBoundary output
          ++ "</td><td>(" ++ render action ++ ")</td></tr>")
         ++ go "" ds
+    go cell1 (ActionNotApplied _ _ : ds) = go cell1 ds
     go cell1 (ReportWord w : ds) =
         ("<tr><td>" ++ cell1 ++ "</td><td>&rarr;</td><td>"
          ++ concatWithBoundary w
@@ -878,11 +907,14 @@ reportAsText
     :: (r -> String)  -- ^ Specifies how to pretty-print actions as text
     -> Log r -> String
-reportAsText render item = unlines $
-    concatWithBoundary (inputWord item) : fmap toLine (alignWithPadding $ derivations item)
+reportAsText render item
+    | all noApplication (derivations item) = input ++ " (no change)"
+    | otherwise = unlines $ input : fmap toLine (alignWithPadding $ derivations item)
   where
+    input = concatWithBoundary (inputWord item)
+
     alignWithPadding ds =
-        let (rawOutputs, actions) = unzip $ toPrintable <$> ds
+        let (rawOutputs, actions) = unzip $ mapMaybe toPrintable ds
             outputs = maybe "(deleted)" concatWithBoundary <$> rawOutputs
             maxlen = maximum $ length <$> outputs
             padded = outputs <&> \o -> o ++ replicate (maxlen - length o) ' '
@@ -890,8 +922,9 @@ 
     toLine (output, action) = "  -> " ++ output ++ "  (" ++ action ++ ")"
 
-    toPrintable (ActionApplied a o) = (o, render a)
-    toPrintable (ReportWord w) = (Just w, "report")
+    toPrintable (ActionApplied a o) = Just (o, render a)
+    toPrintable (ActionNotApplied _ _) = Nothing
+    toPrintable (ReportWord w) = Just (Just w, "report")
 
 -- | Apply a single 'Statement' to a word. Returns a 'LogItem' for
 -- each possible result, or @[]@ if the rule does not apply and the
@@ -903,7 +936,7 @@ applyStatement ReportS w = [ReportWord w]
 applyStatement st w = case applyStatementStr st w of
     [] -> [ActionApplied st Nothing]
-    [w'] | w' == w -> []
+    [w'] | w' == w -> [ActionNotApplied st w']
     r -> ActionApplied st . Just <$> r
 
 -- | Apply a set of 'SoundChanges' to a word, returning a log of which
@@ -927,7 +960,15 @@                     Just w'' -> (l :) <$> go sts w''
                     -- apply no further changes to a deleted word
                     Nothing -> [[l]]
+                l@(ActionNotApplied _ w'') -> (l :) <$> go sts w''
 
+-- | Returns the list of sound changes which did /not/ apply in the
+-- given sound change log.
+rulesNotApplied :: Log r -> [r]
+rulesNotApplied =
+    mapMaybe (\case ActionNotApplied r _ -> Just r; _ -> Nothing)
+    . derivations
+
 -- | Returns the final output from a sound change log.
 getOutput :: Log r -> Maybe PWord
 getOutput l = case derivations l of
@@ -937,46 +978,51 @@ -- | Returns, in order: the input word, any intermediate results from
 -- 'ReportS', and then the final output.
 getReports :: Log r -> [PWord]
-getReports l = inputWord l : go (derivations l)
+getReports l = inputWord l : go Nothing (derivations l)
   where
-    go [] = []
-    go [ActionApplied _ (Just w')] = [w']
-    go (ReportWord w':ls) = w' : go ls
-    go (_:ls) = go ls
+    go w [] = maybeToList w
+    go _ (ActionApplied    _ w' : ls) = go w' ls
+    go _ (ActionNotApplied _ w' : ls) = go (Just w') ls
+    go _ (ReportWord w':ls) = w' : go (Just w') ls
 
+data HighlightMode = AllChanged | SpecificRule
+    deriving (Show, Eq)
+
+-- | Based on its flags, should this rule be highlighted in the given 'HighlightMode'?
+needsHighlight :: HighlightMode -> Rule c -> Bool
+needsHighlight m rule = case m of
+    AllChanged -> highlightChanges $ flags rule
+    SpecificRule -> highlightSpecificRule $ flags rule
+
 -- | Returns the final output from a sound change log, as well as an
 -- indication of whether any sound changes have applied to it
--- (accounting for 'highlightChanges' flags).
-getChangedOutputs :: Log (Statement c d) -> Maybe (PWord, Bool)
-getChangedOutputs l = case derivations l of
+-- (accounting for highlighting flags).
+getChangedOutputs :: HighlightMode -> Log (Statement c d) -> Maybe (PWord, Bool)
+getChangedOutputs m l = case derivations l of
     [] -> Just (inputWord l, False)
     logs -> case logOutput (last logs) of
         Just out -> Just (out, hasChanged logs)
         Nothing -> Nothing
   where
     hasChanged = any $ \case
-        ActionApplied (RuleS rule) _ -> highlightChanges $ flags rule
+        ActionApplied (RuleS rule) _ -> needsHighlight m rule
         ActionApplied (FilterS _) _ -> False  -- cannot highlight nonexistent word
         ActionApplied (DeclS _) _ -> True
         ActionApplied ReportS _ -> False  -- reporting a word yields no change
+        ActionNotApplied _ _ -> False
         ReportWord _ -> False
 
--- | A combination of 'getOutput' and 'getChangedOutputs': returns all
+-- | A combination of 'getReports' and 'getChangedOutputs': returns all
 -- intermediate results, as well as whether each has undergone any
 -- sound changes.
-getChangedReports :: Log (Statement c d) -> [(PWord, Bool)]
-getChangedReports l = (inputWord l, False) : case derivations l of
-    [] -> []
-    ls -> go False ls
+getChangedReports :: HighlightMode -> Log (Statement c d) -> [(PWord, Bool)]
+getChangedReports m l = (inputWord l, False) : go False Nothing (derivations l)
   where
-    go _ [] = []
-    go hasChanged (ActionApplied action _:ls) =
+    go hasChanged w [] = maybeToList $ w <&> (,hasChanged)
+    go hasChanged _ (ActionApplied action w':ls) =
         let hasChanged' = case action of
-                RuleS rule -> hasChanged || highlightChanges (flags rule)
+                RuleS rule -> hasChanged || needsHighlight m rule
                 _ -> hasChanged
-        in go hasChanged' ls
-    go hasChanged (ReportWord w':ls) = (w', hasChanged) : go hasChanged ls
-
--- | Apply a set of 'SoundChanges' to a word, returning the final
--- output word(s) as well as any intermediate results from 'ReportS',
--- each with a boolean marking changed results (as with 'applyChangesWithChanges').
+        in go hasChanged' w' ls
+    go hasChanged _ (ActionNotApplied _ w':ls) = go hasChanged (Just w') ls
+    go hasChanged _ (ReportWord w':ls) = (w', hasChanged) : go hasChanged (Just w') ls
src/Brassica/SoundChange/Expand.hs view
@@ -4,6 +4,7 @@ {-# LANGUAGE GADTs #-}
 {-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE TupleSections #-}
 
 -- |
@@ -43,11 +44,11 @@ import Prelude hiding (lookup)
 import Control.DeepSeq (NFData)
 import Control.Monad (foldM, unless)
-import Control.Monad.State.Strict (StateT, evalStateT, lift, get, put, gets)
+import Control.Monad.State.Strict (StateT, evalStateT, lift, get, gets, modify)
 import Data.Bifunctor (first, second)
 import Data.Containers.ListUtils (nubOrd)
 import Data.List (transpose, foldl', stripPrefix)
-import Data.Maybe (mapMaybe, catMaybes)
+import Data.Maybe (mapMaybe)
 import Data.Traversable (for)
 import GHC.Generics (Generic)
 
@@ -259,13 +260,14 @@     <*> traverse expandEnvironment (exception r)
     <*> pure (flags r)
     <*> pure (plaintext r)
+    <*> pure (loc r)
   where
     expandEnvironment (e1, e2) = (,)
         <$> traverse (expandLexeme cs) e1
         <*> traverse (expandLexeme cs) e2
 
 expandFilter :: Categories -> Filter CategorySpec -> Either ExpandError (Filter Expanded)
-expandFilter cs (Filter p f) = Filter p <$> traverse (expandLexeme cs) f
+expandFilter cs (Filter p l f) = Filter p l <$> traverse (expandLexeme cs) f
 
 -- | Extend a set of previously defined t'Categories' to give the
 -- resulting state after a v'Categories' directive.
@@ -321,6 +323,12 @@     mkAuto :: String -> String -> [String] -> (String, Either (Expanded 'AnyPart) AutosegmentDef)
     mkAuto f g gs = (g, Right $ AutosegmentDef f gs)
 
+data ExpansionState = ExpansionState
+    { categories :: Categories
+    , extras :: [String]
+    , persistents :: [(String, Rule Expanded)]
+    } deriving (Show)
+
 -- | Expand a set of 'SoundChanges'. Expansion proceeds from beginning
 -- to end as follows:
 --
@@ -337,37 +345,55 @@ --       currently defined graphemes. They are replaced with a
 --       'GraphemeList' only if no categories are defined in the
 --       'SoundChanges'.
+--
+-- On an error, returns the appropriate 'ExpandError' together with
+-- the line number of the offending statement (if it has one).
 expandSoundChanges
     :: SoundChanges CategorySpec Directive
-    -> Either ExpandError (SoundChanges Expanded GraphemeList)
-expandSoundChanges scs = fmap catMaybes $ flip evalStateT (M.empty, []) $ traverse go scs
+    -> Either (Maybe Int, ExpandError) (SoundChanges Expanded GraphemeList)
+expandSoundChanges scs = fmap concat $ flip evalStateT initState $ traverse go scs
   where
     noCategories = any (\case DeclS (Categories {}) -> True; _ -> False) scs
 
+    initState = ExpansionState M.empty [] []
+
     go  :: Statement CategorySpec Directive
         -> StateT
-            (Categories, [String])
-            (Either ExpandError)
-            (Maybe (Statement Expanded GraphemeList))
+            ExpansionState
+            (Either (Maybe Int, ExpandError))
+            [Statement Expanded GraphemeList]
     go (RuleS r) = do
-        cs <- gets fst
-        lift $ Just . RuleS <$> expandRule cs r
-    go (FilterS f) = do
-        cs <- gets fst
-        lift $ Just . FilterS <$> expandFilter cs f
-    go ReportS = pure (Just ReportS)
+        ExpansionState{..} <- get
+        st <- lift $ first (Just $ loc r,) (expandRule categories r)
+        case persistent (flags st) of
+            Nothing -> pure ()
+            Just label ->
+                modify $ \s -> s
+                    { persistents = (label, st) : persistents
+                    }
+        -- NB. 'persistents' here doesn't contain current rule
+        pure $ RuleS <$> (st : reverse (snd <$> persistents))
+    go (FilterS f@(Filter _ l _)) = do
+        cs <- gets categories
+        lift $ pure . FilterS <$> first (Just l,) (expandFilter cs f)
+    go ReportS = pure [ReportS]
     go (DeclS (ExtraGraphemes extra)) = do
-        (cs, _) <- get
-        put (cs, extra)
+        modify $ \s -> s { extras = extra }
         pure $
             if noCategories
-            then Just $ DeclS $ GraphemeList True extra
-            else Nothing
-    go (DeclS (Categories overwrite noreplace defs)) = do
-        (cs, extra) <- get
-        cs' <- lift $ extendCategories cs (overwrite, defs)
-        put (cs', extra)
-        pure $ Just $ DeclS $ GraphemeList noreplace $ extra ++ mapMaybe grapheme (values cs')
+            then [DeclS $ GraphemeList True extra]
+            else []
+    go (DeclS (Categories l overwrite noreplace defs)) = do
+        ExpansionState{..} <- get
+        cs' <- lift $ first (Just l,) (extendCategories categories (overwrite, defs))
+        modify $ \s -> s
+            { categories = cs'
+            , persistents = []  -- invalidate persistent rules if categories change
+            }
+        pure $ pure $ DeclS $ GraphemeList noreplace $ extras ++ mapMaybe grapheme (values cs')
+    go (DeclS (DisablePersistent label)) = do
+        modify $ \s -> s { persistents = filter ((/=label) . fst) (persistents s) }
+        pure []
 
     grapheme [Grapheme g] = Just g
     grapheme _ = Nothing
src/Brassica/SoundChange/Frontend/Internal.hs view
@@ -1,9 +1,15 @@ {-# LANGUAGE DeriveAnyClass  #-}
 {-# LANGUAGE DeriveGeneric   #-}
 {-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE DuplicateRecordFields #-}
 {-# LANGUAGE LambdaCase      #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RankNTypes      #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE TupleSections   #-}
+{-# OPTIONS_GHC -Wno-incomplete-record-updates #-}
 
 -- |
 -- Module      : Brassica.SoundChange.Frontend.Internal
@@ -23,55 +29,63 @@ module Brassica.SoundChange.Frontend.Internal where
 
 import Control.Monad ((<=<))
+import Control.Parallel.Strategies (withStrategy, parTraversable, rseq)
+import Data.Aeson (FromJSON(..), ToJSON(..), Value (..))
+import Data.Aeson.TH (deriveJSON, defaultOptions, defaultTaggedObject, constructorTagModifier, sumEncoding, tagFieldName)
+import Data.Aeson.Types (prependFailure, typeMismatch)
 import Data.Containers.ListUtils (nubOrd)
-import Data.List (transpose, intersperse)
-import Data.Maybe (fromMaybe, mapMaybe)
+import Data.Foldable (toList)
+import Data.List (transpose, intersperse, intersectBy)
+import qualified Data.List.NonEmpty as NE
+import Data.Maybe (fromMaybe, mapMaybe, maybeToList)
 import Data.Void (Void)
+import Data.Text (unpack)
 import GHC.Generics (Generic)
 import Myers.Diff (getDiff, PolyDiff(..))
 
 import Control.DeepSeq (NFData)
-import Text.Megaparsec (ParseErrorBundle)
+import Text.Megaparsec (reachOffsetNoLine, errorOffset, PosState (..), SourcePos (..), unPos)
+import Text.Megaparsec.Error (ParseErrorBundle(..))
 
 import Brassica.SFM.MDF
 import Brassica.SFM.SFM
-import Brassica.SoundChange.Apply
+import Brassica.SoundChange (ExpandError(..), parseSoundChanges, errorBundlePretty, expandSoundChanges)
+import Brassica.SoundChange.Apply hiding (HighlightMode)
+import qualified Brassica.SoundChange.Apply as A
 import Brassica.SoundChange.Tokenise
 import Brassica.SoundChange.Types
+import Brassica.Paradigm (parseParadigm, parseStems, formatNested, ResultsTree (..), applyParadigm)
 
 -- | Rule application mode of the SCA.
 data ApplicationMode
     = ApplyRules HighlightMode OutputMode String
     -- ^ Apply sound changes as normal, with the given modes and
     -- separator
-    | ReportRulesApplied
-    -- ^ Apply reporting the rules which were applied (as HTML)
+    | ReportRules ReportMode
+    -- ^ Apply reporting rules (as HTML)
     deriving (Show, Eq)
 
+data ReportMode
+    = ReportApplied
+    -- ^ Report the rules which were applied
+    | ReportNotApplied
+    -- ^ Report the rules which were not applied
+    deriving (Show, Eq)
+
 -- | Get the 'OutputMode' if one is set, otherwise default to
 -- 'WordsOnlyOutput'.
 getOutputMode :: ApplicationMode -> OutputMode
 getOutputMode (ApplyRules _ o _) = o
-getOutputMode ReportRulesApplied = WordsOnlyOutput
+getOutputMode (ReportRules _) = WordsOnlyOutput
 
 -- | Mode for highlighting output words
 data HighlightMode
     = NoHighlight
     | DifferentToLastRun
-    | DifferentToInput
+    | DifferentToInput A.HighlightMode
     -- ^ NB. now labeled ‘any rule applied’ in GUI
     deriving (Show, Eq)
-instance Enum HighlightMode where
-    -- used for conversion to and from C, so want control over values
-    fromEnum NoHighlight = 0
-    fromEnum DifferentToLastRun = 1
-    fromEnum DifferentToInput = 2
 
-    toEnum 0 = NoHighlight
-    toEnum 1 = DifferentToLastRun
-    toEnum 2 = DifferentToInput
-    toEnum _ = undefined
-
 -- | Mode for reporting output words (and sometimes intermediate and
 -- input words too)
 data OutputMode
@@ -81,27 +95,14 @@     | WordsWithProtoOutput
     | WordsWithProtoOutputPreserve
     deriving (Show, Eq)
-instance Enum OutputMode where
-    -- used for conversion to and from C, so want control over values
-    fromEnum MDFOutput = 0
-    fromEnum WordsOnlyOutput = 1
-    fromEnum MDFOutputWithEtymons = 2
-    fromEnum WordsWithProtoOutput = 3
-    fromEnum WordsWithProtoOutputPreserve = 4
 
-    toEnum 0 = MDFOutput
-    toEnum 1 = WordsOnlyOutput
-    toEnum 2 = MDFOutputWithEtymons
-    toEnum 3 = WordsWithProtoOutput
-    toEnum 4 = WordsWithProtoOutputPreserve
-    toEnum _ = undefined
-
 -- | Output of a single application of rules to a wordlist: either a
 -- list of possibly highlighted words, an applied rules table, or a
 -- parse error.
 data ApplicationOutput a r
     = HighlightedWords [Component (a, Bool)]
     | AppliedRulesTable [Log r]
+    | NotAppliedRulesList [Rule Expanded]
     | ParseError (ParseErrorBundle String Void)
     deriving (Show, Generic, NFData)
 
@@ -112,17 +113,7 @@ -- | Kind of input: either a raw wordlist, or an MDF file.
 data InputLexiconFormat = Raw | MDF MDFHierarchy
     deriving (Show, Eq)
-instance Enum InputLexiconFormat where
-    -- used for conversion to and from C, so want control over values
-    fromEnum Raw = 0
-    fromEnum (MDF Standard) = 1
-    fromEnum (MDF Alternate) = 2
 
-    toEnum 0 = Raw
-    toEnum 1 = MDF Standard
-    toEnum 2 = MDF Alternate
-    toEnum _ = undefined
-
 -- | Either a list of 'Component's for a Brassica wordlist file, or a
 -- list of 'SFM' fields for an MDF file
 data ParseOutput a = ParsedRaw [Component a] | ParsedMDF SFM
@@ -171,9 +162,14 @@     case tokeniseAccordingToInputFormat intype (getOutputMode mode) statements ws of
         Left e -> ParseError e
         Right toks -> case mode of
-            ReportRulesApplied ->
+            ReportRules ReportApplied ->
                 AppliedRulesTable $ concat $
                     getWords $ parFmap (applyChanges statements) toks
+            ReportRules ReportNotApplied ->
+                let ws' = getWords $ parFmap (applyChanges statements) toks
+                    na = mapMaybe tagLOC . rulesNotApplied <$> concat ws'
+                in NotAppliedRulesList $ intersectByTag na
+                -- let is = intersect' $ getWords $ parFmap (rulesNotApplied statements) toks
             ApplyRules DifferentToLastRun mdfout sep ->
                 let result = concatMap (splitMultipleResults sep) $
                         joinComponents' mdfout $ parFmap (doApply mdfout statements) toks
@@ -181,9 +177,9 @@                     mapMaybe polyDiffToHighlight $ getDiff (fromMaybe [] prev) result
                     -- zipWithComponents result (fromMaybe [] prev) [] $ \thisWord prevWord ->
                     --     (thisWord, thisWord /= prevWord)
-            ApplyRules DifferentToInput mdfout sep ->
+            ApplyRules (DifferentToInput m) mdfout sep ->
                 HighlightedWords $ concatMap (splitMultipleResults sep) $
-                        joinComponents' mdfout $ parFmap (doApplyWithChanges mdfout statements) toks
+                        joinComponents' mdfout $ parFmap (doApplyWithChanges m mdfout statements) toks
             ApplyRules NoHighlight mdfout sep ->
                 HighlightedWords $ (fmap.fmap) (,False) $ concatMap (splitMultipleResults sep) $
                     joinComponents' mdfout $ parFmap (doApply mdfout statements) toks
@@ -201,6 +197,14 @@     unsafeCastComponent (Separator s) = Separator s
     unsafeCastComponent (Gloss s) = Gloss s
 
+    intersectByTag :: Eq a => [[(a, b)]] -> [b]
+    intersectByTag [] = []
+    intersectByTag xs = snd <$> foldr1 (intersectBy (\x y -> fst x == fst y)) xs
+
+    tagLOC :: Statement Expanded GraphemeList -> Maybe (Int, Rule Expanded)
+    tagLOC (RuleS r) = Just (loc r, r)
+    tagLOC _ = Nothing
+
     doApply :: OutputMode -> SoundChanges Expanded GraphemeList -> PWord -> [Component [PWord]]
     doApply WordsWithProtoOutput scs w = doApplyWithProto scs w
     doApply WordsWithProtoOutputPreserve scs w = doApplyWithProto scs w
@@ -211,14 +215,14 @@             intermediates = fmap nubOrd $ transpose $ getReports <$> applyChanges scs w
         in intersperse (Separator " → ") (fmap Word intermediates)
 
-    doApplyWithChanges :: OutputMode -> SoundChanges Expanded GraphemeList -> PWord -> [Component [(PWord, Bool)]]
-    doApplyWithChanges WordsWithProtoOutput scs w = doApplyWithChangesWithProto scs w
-    doApplyWithChanges WordsWithProtoOutputPreserve scs w = doApplyWithChangesWithProto scs w
-    doApplyWithChanges _ scs w = [Word $ mapMaybe getChangedOutputs $ applyChanges scs w]
+    doApplyWithChanges :: A.HighlightMode -> OutputMode -> SoundChanges Expanded GraphemeList -> PWord -> [Component [(PWord, Bool)]]
+    doApplyWithChanges m WordsWithProtoOutput scs w = doApplyWithChangesWithProto m scs w
+    doApplyWithChanges m WordsWithProtoOutputPreserve scs w = doApplyWithChangesWithProto m scs w
+    doApplyWithChanges m _ scs w = [Word $ mapMaybe (getChangedOutputs m) $ applyChanges scs w]
 
-    doApplyWithChangesWithProto scs w =
+    doApplyWithChangesWithProto m scs w =
         let intermediates :: [[(PWord, Bool)]]
-            intermediates = fmap nubOrd $ transpose $ getChangedReports <$> applyChanges scs w
+            intermediates = fmap nubOrd $ transpose $ getChangedReports m <$> applyChanges scs w
         in intersperse (Separator " → ") (fmap Word intermediates)
 
     joinComponents' WordsWithProtoOutput =
@@ -234,3 +238,147 @@     linespace (c:cs@(Separator _:_)) = c : linespace cs
     linespace (c:cs) = c : Separator "\n" : linespace cs
     linespace [] = []
+
+getErrorLocs :: ParseErrorBundle String Void -> [Int]
+getErrorLocs ParseErrorBundle { bundleErrors, bundlePosState } =
+    go (NE.toList bundleErrors) bundlePosState
+  where
+    -- based on Text.Megaparsec.Error.errorBundlePrettyWith
+    go (e:es) pst =
+        let pst' = reachOffsetNoLine (errorOffset e) pst
+            l = unPos $ sourceLine $ pstateSourcePos pst'
+        in l : go es pst'
+    go [] _ = []
+
+
+-------- JSON server
+
+
+data Request
+    = ReqRules
+        { changes :: String
+        , input :: String
+        , report :: Maybe ReportMode
+        , inFmt :: InputLexiconFormat
+        , hlMode :: HighlightMode
+        , outMode :: OutputMode
+        , prev :: Maybe [Component PWord]
+        , sep :: String
+        , reqTimeout :: Int   -- ^ microseconds
+        }
+    | ReqParadigm
+        { pText :: String
+        , input :: String
+        , separateLines :: Bool
+        , reqTimeout :: Int  -- ^ microseconds
+        }
+    deriving (Show)
+
+data Response
+    = RespRules
+        { prev :: Maybe [Component PWord]
+        , output :: String
+        }
+    | RespParadigm
+        { output :: String
+        }
+    | RespNotApplied
+        { highlights :: [Int]
+        }
+    | RespError
+        { highlights :: [Int]
+        , message :: String
+        }
+    deriving (Show, Generic, NFData)
+
+instance ToJSON InputLexiconFormat where
+    toJSON Raw = "Raw"
+    toJSON (MDF Standard) = "MDFStandard"
+    toJSON (MDF Alternate) = "MDFAlternate"
+
+instance FromJSON InputLexiconFormat where
+    parseJSON (String "Raw") = pure Raw
+    parseJSON (String "MDFStandard") = pure $ MDF Standard
+    parseJSON (String "MDFAlternate") = pure $ MDF Alternate
+    parseJSON (String s) = fail $ "Unknown InputLexiconFormat: " ++ unpack s
+    parseJSON invalid = prependFailure "parsing InputLexiconFormat failed: " $
+        typeMismatch "String" invalid
+
+instance FromJSON HighlightMode where
+    parseJSON (String "NoHighlight") = pure NoHighlight
+    parseJSON (String "DifferentToLastRun") = pure DifferentToLastRun
+    parseJSON (String "DifferentToInputAllChanged") = pure $ DifferentToInput AllChanged
+    parseJSON (String "DifferentToInputSpecificRule") = pure $ DifferentToInput SpecificRule
+    parseJSON invalid = prependFailure "parsing HighlightMode failed: " $
+        typeMismatch "String" invalid
+
+instance ToJSON HighlightMode where
+    toJSON NoHighlight = "NoHighlight"
+    toJSON DifferentToLastRun = "DifferentToLastRun"
+    toJSON (DifferentToInput AllChanged) = "DifferentToInputAllChanged"
+    toJSON (DifferentToInput SpecificRule) = "DifferentToInputSpecificRule"
+
+$(deriveJSON defaultOptions ''OutputMode)
+$(deriveJSON defaultOptions ''ReportMode)
+
+$(deriveJSON defaultOptions{constructorTagModifier=drop 3, sumEncoding=defaultTaggedObject{tagFieldName="method"}} ''Request)
+$(deriveJSON defaultOptions{constructorTagModifier=drop 4, sumEncoding=defaultTaggedObject{tagFieldName="method"}} ''Response)
+
+
+dispatch :: Request -> Response
+dispatch r = case r of
+    ReqRules{} -> parseTokeniseAndApplyRulesWrapper r
+    ReqParadigm{} -> parseAndBuildParadigmWrapper r
+  where
+    parseTokeniseAndApplyRulesWrapper
+        :: Request
+        -> Response
+    parseTokeniseAndApplyRulesWrapper ReqRules{..} =
+        let mode = maybe (ApplyRules hlMode outMode sep) ReportRules report
+        in case parseSoundChanges changes of
+            Left e -> RespError (getErrorLocs e) $ "<pre>" ++ errorBundlePretty e ++ "</pre>"
+            Right statements ->
+                case expandSoundChanges statements of
+                    Left (loc, err) -> RespError (maybeToList loc) $ ("<pre>"++) $ (++"</pre>") $ case err of
+                        (NotFound s) -> "Could not find category: " ++ s
+                        InvalidBaseValue -> "Invalid value used as base grapheme in feature definition"
+                        InvalidDerivedValue -> "Invalid value used as derived grapheme in autosegment"
+                        MismatchedLengths -> "Mismatched lengths in feature definition"
+                    Right statements' ->
+                        let result' = parseTokeniseAndApplyRules parFmap statements' input inFmt mode prev
+                        in case result' of
+                            ParseError e -> RespError [] $
+                                "<pre>" ++ errorBundlePretty e ++ "</pre>"
+                            HighlightedWords result -> RespRules
+                                (Just $ (fmap.fmap) fst result)
+                                (escape $ detokeniseWords' highlightWord result)
+                            AppliedRulesTable items -> RespRules Nothing $
+                                concatMap (surroundTable . reportAsHtmlRows plaintext') items
+                            NotAppliedRulesList items -> RespNotApplied $ loc <$> items
+      where
+        highlightWord (s, False) = concatWithBoundary s
+        highlightWord (s, True) = "<b>" ++ concatWithBoundary s ++ "</b>"
+
+        surroundTable :: String -> String
+        surroundTable s = "<table>" ++ s ++ "</table>"
+    parseTokeniseAndApplyRulesWrapper _ = error "parseTokeniseAndApplyRulesWrapper: unexpected request!"
+
+    parseAndBuildParadigmWrapper :: Request -> Response
+    parseAndBuildParadigmWrapper ReqParadigm{..} =
+        case parseParadigm pText of
+            Left e -> RespError [] $ "<pre>" ++ errorBundlePretty e ++ "</pre>"
+            Right p -> RespParadigm $ escape $
+                (if separateLines
+                    then unlines . toList
+                    else formatNested id)
+                $ Node $ uncurry (applyParadigm p) . parseStems <$> lines input
+    parseAndBuildParadigmWrapper _ = error "parseAndBuildParadigmWrapper: unexpected request!"
+
+    escape :: String -> String
+    escape = concatMap $ \case
+        '\n' -> "<br/>"
+        -- '\t' -> "&#9;"  -- this doesn't seem to do anything - keeping it here in case I eventually figure out how to do tabs in Qt
+        c    -> pure c
+
+parFmap :: (a -> b) -> [Component a] -> [Component b]
+parFmap f = withStrategy (parTraversable rseq) . fmap (fmap f)
src/Brassica/SoundChange/Parse.hs view
@@ -13,7 +13,7 @@ -- -- Functions to parse sound changes in Brassica syntax. (For details -- on the syntax, refer to the--- [reference guide](https://github.com/bradrn/brassica/blob/v1.0.0/docs/Reference.md).)+-- [reference guide](https://github.com/bradrn/brassica/blob/v1.1.0/docs/Reference.md).) module Brassica.SoundChange.Parse     ( parseRule     , parseSoundChanges@@ -21,10 +21,10 @@     , module Text.Megaparsec.Error     ) where -import Data.Char (isSpace)+import Data.Char (isSpace, isAlphaNum) import Data.Foldable (asum) import Data.List (dropWhileEnd)-import Data.Maybe (isNothing, isJust, fromJust, fromMaybe)+import Data.Maybe (isJust, fromJust, fromMaybe) import Data.Void (Void)  import Control.Applicative.Permutations@@ -110,7 +110,9 @@ parseCategoryStandalone = do     g <- parseGrapheme' True     _ <- symbol "="-    mods <- some (parseCategoryModification False)+    mods <- (:)+        <$> parseCategoryModification True+        <*> many (parseCategoryModification False)     return (g, CategorySpec mods)  parseFeature :: Parser FeatureSpec@@ -142,12 +144,13 @@         <|> pure Union  parseDirective :: Parser Directive-parseDirective = parseCategoriesDirective <|> parseExtraDirective+parseDirective = parseCategoriesDirective <|> parseExtraDirective <|> parsePStop   where     parseExtraDirective = fmap ExtraGraphemes $         symbol "extra" *> many (parseGrapheme' False) <* scn      parseCategoriesDirective = do+        loc <- unPos . sourceLine <$> getSourcePos         overwrite <- isJust <$> optional (symbol "new")         _ <- symbol "categories"         noreplace <- isJust <$> optional (symbol "noreplace")@@ -157,8 +160,11 @@             DefineAuto <$> parseAuto <|>             uncurry DefineCategory <$> (try parseCategoryStandalone <* scn)         _ <- symbol "end" <* scn-        pure $ Categories overwrite noreplace cs+        pure $ Categories loc overwrite noreplace cs +    parsePStop = fmap DisablePersistent $+        symbol "pstop" *> takeWhile1P (Just "alphanumeric character") isAlphaNum <* scn+ parseOptional :: ParseLexeme a => Parser (Lexeme CategorySpec a) parseOptional = Optional <$> between (symbol "(") (symbol ")") (some parseLexeme) @@ -242,12 +248,15 @@  parseFlags :: Parser Flags parseFlags = runPermutation $ Flags-    <$> toPermutation (isNothing <$> optional (symbol "-x"))+    <$> toPermutationWithDefault True (False <$ symbol "-x")+    <*> toPermutationWithDefault False (True <$ symbol "-h")     <*> toPermutationWithDefault LTR ((LTR <$ symbol "-ltr") <|> (RTL <$ symbol "-rtl"))-    <*> toPermutation (isJust <$> optional (symbol "-1"))+    <*> toPermutationWithDefault False (True <$ symbol "-1")     <*> toPermutationWithDefault ApplyAlways         ((PerApplication <$ symbol "-??") <|> (PerWord <$ symbol "-?"))-    <*> toPermutation (isJust <$> optional (symbol "-no"))+    <*> toPermutationWithDefault False (True <$ symbol "-no")+    <*> toPermutationWithDefault Nothing+        (fmap Just $ symbol "-pstart" *> lexeme (takeWhile1P (Just "alphanumeric character") isAlphaNum))  ruleParser :: Parser (Rule CategorySpec) ruleParser = do@@ -256,6 +265,7 @@     -- to wrap multiple parsers rather than just one     o <- getOffset     s <- getInput+    loc <- unPos . sourceLine <$> getSourcePos      flags <- parseFlags     target <- manyTill parseLexeme $ lexeme $ choice@@ -268,13 +278,12 @@     envs' <- many $ do         notFollowedBy $ symbol "//"  -- for exceptions         _ <- symbol "/"-        env1 <- parseLexemes-        _ <- symbol "_"-        env2 <- parseLexemes-        return (env1, env2)+        parseEnv     let envs = if null envs' then [([], [])] else envs' -    exception <- optional $ (,) <$> (symbol "//" *> parseLexemes) <* symbol "_" <*> parseLexemes+    exception <-+        (symbol "//" *> ((:) <$> parseEnv <*> many (symbol "/" *> parseEnv)))+        <|> pure []      o' <- getOffset @@ -283,9 +292,18 @@     let plaintext = dropWhile isSpace $ dropWhileEnd isSpace $             (fst . fromJust) (takeN_ (o' - o) s)     return Rule{environment=envs, ..}+  where+    parseEnv = do+        env1 <- parseLexemes+        _ <- symbol "_"+        env2 <- parseLexemes+        return (env1, env2)  filterParser :: Parser (Filter CategorySpec)-filterParser = fmap (uncurry Filter) $ match $ symbol "filter" *> parseLexemes <* optional scn+filterParser = do+    loc <- unPos . sourceLine <$> getSourcePos+    (p, f) <- match $ symbol "filter" *> parseLexemes <* optional scn+    pure $ Filter p loc f  -- Space handline is a little complex here: we want to make sure that -- 'report' is always on its own line, but can have as much or as
src/Brassica/SoundChange/Tokenise.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE DeriveGeneric     #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE LambdaCase        #-}+{-# LANGUAGE TemplateHaskell   #-} {-# LANGUAGE TypeFamilies      #-} {-# LANGUAGE ViewPatterns      #-} @@ -34,6 +35,7 @@        , sortByDescendingLength        ) where +import Data.Aeson.TH (deriveJSON, defaultOptions) import Data.Char (isSpace) import Data.Function (on) import Data.Functor.Identity@@ -56,6 +58,8 @@     | Separator String  -- ^ A separator, e.g. whitespace     | Gloss String      -- ^ A gloss (in Brassica syntax, between square brackets)     deriving (Eq, Show, Functor, Foldable, Traversable, Generic, NFData)++$(deriveJSON defaultOptions ''Component)  -- | Flatten a nested list of 'Component's. joinComponents :: [Component [Component a]] -> [Component a]
src/Brassica/SoundChange/Types.hs view
@@ -33,7 +33,7 @@ --     * Each word is a sequence of t'Grapheme's -- -- For more details on the syntax and semantics of sound changes,--- refer to the [reference guide](https://github.com/bradrn/brassica/blob/v1.0.0/docs/Reference.md).+-- refer to the [reference guide](https://github.com/bradrn/brassica/blob/v1.1.0/docs/Reference.md). module Brassica.SoundChange.Types        (        -- * Words and graphemes@@ -121,7 +121,7 @@ -- to 'Matched' or 'Replacement' positions respectively. -- -- For details on the syntax and semantics of each kind of lexeme,--- refer to the [reference guide](https://github.com/bradrn/brassica/blob/v1.0.0/docs/Reference.md).+-- refer to the [reference guide](https://github.com/bradrn/brassica/blob/v1.1.0/docs/Reference.md). data Lexeme category (a :: LexemeType) where     Grapheme :: Grapheme -> Lexeme category a     Category :: category a -> Lexeme category a@@ -275,10 +275,12 @@ -- change how it is applied. data Flags = Flags   { highlightChanges :: Bool         -- ^ Whether results from this sound change can be highlighted in a GUI+  , highlightSpecificRule :: Bool     -- ^ Whether to highlight words changed by this rule in particular (when the appropriate option is set)   , applyDirection   :: Direction    -- ^ Direction in which to apply the rule   , applyOnceOnly    :: Bool         -- ^ Whether to apply the rule only once to a word   , sporadic         :: Sporadicity  -- ^ Whether the rule should be applied sporadically, and if so, how   , nonOverlappingTarget :: Bool     -- ^ Whether the rule should apply non-iteratively (avoiding environments which overlap with targets)+  , persistent       :: Maybe String -- ^ Whether to continue applying the rule persistently (with a label to disable the rule eventually)   } deriving (Show, Generic, NFData)  -- | A default selection of flags which are appropriate for most@@ -299,31 +301,35 @@ defFlags :: Flags defFlags = Flags     { highlightChanges = True+    , highlightSpecificRule = False     , applyDirection = LTR     , applyOnceOnly = False     , sporadic = ApplyAlways     , nonOverlappingTarget = False+    , persistent = Nothing     }  -- | A single sound change rule. -- -- In Brassica sound-change syntax with all elements specified, this would be--- @-flags target / replacement \/ environment1 \/ environment2 \/ … \/ exception@.+-- @-flags target / replacement \/ environment1 \/ environment2 \/ … \/\/ exception1 \/ exception2 \/ …@. data Rule c = Rule   { target      :: [Lexeme c 'Matched]   , replacement :: [Lexeme c 'Replacement]   , environment :: [Environment c]-  , exception   :: Maybe (Environment c)+  , exception   :: [Environment c]   , flags       :: Flags   , plaintext   :: String  -- ^ Rule text before parsing (displayed e.g. for debugging purposes)+  , loc         :: Int     -- ^ Line number of rule in original source file (1-indexed!)   } deriving (Generic)  deriving instance (forall a. Show (c a)) => Show (Rule c) deriving instance (forall a. NFData (c a)) => NFData (Rule c) --- | A filter, constraining the output to not match the given elements.--- (The 'String' is the plaintext, as with 'Rule'.)-data Filter c = Filter String [Lexeme c 'Matched]+-- | A filter, constraining the output to not match the given+-- elements.  (The 'String' is the plaintext and the 'Int' is the line+-- number, as with 'Rule'.)+data Filter c = Filter String Int [Lexeme c 'Matched]     deriving (Generic)  deriving instance (forall a. Show (c a)) => Show (Filter c)@@ -350,7 +356,7 @@ -- @"\<declaration\>"@ for all 'DeclS' inputs. plaintext' :: Statement c decl -> String plaintext' (RuleS r) = plaintext r-plaintext' (FilterS (Filter p _)) = p+plaintext' (FilterS (Filter p _ _)) = p plaintext' ReportS = "intermediate result" plaintext' (DeclS _) = "<declaration>" @@ -412,11 +418,13 @@ -- something for later use. data Directive     = Categories  -- ^ Category definition block+        Int   -- ^ Line number         Bool  -- ^ Whether category was introduced with @new@         Bool  -- ^ Whether category was introduced with @noreplace@         [CategoryDefinition]     | ExtraGraphemes [String]       -- ^ Extra graphemes declaration: @extra …@+    | DisablePersistent String     deriving (Show, Eq, Ord, Generic, NFData)  -- | A list of graphemes, replacing v'Categories' in expanded sound
test/DocTests.hs view
@@ -20,9 +20,9 @@ import Brassica.SoundChange.Tokenise (withFirstCategoriesDecl, tokeniseWord)  main :: IO ()-main = defaultMain $ testGroup "brassica-doctests"-    [ doctest "docs/Writing-Sound-Changes.md" ""-    , doctest "docs/Reference.md" "\+main = do+    t1 <- doctest "docs/Writing-Sound-Changes.md" ""+    t2 <- doctest "docs/Reference.md" "\ \categories noreplace\n\ \C = m n p t ch k b d j g f s sh h v z r l w y\n\ \-Stress = a e i o u\n\@@ -30,52 +30,51 @@ \auto -Stress\n\ \V = &&Stress\n\ \end\n"-    ]+    defaultMain $ testGroup "brassica-doctests" [t1, t2] -doctest :: FilePath -> String -> TestTree-doctest file prefix = singleTest file $ DocTest file prefix+doctest :: FilePath -> String -> IO TestTree+doctest path prefix = do+    doc <- T.readFile path+    tests <- runIO (readMarkdown def { readerExtensions = githubMarkdownExtensions } doc) <&> \case+        Left err -> error $ T.unpack $ renderError err+        Right (Pandoc _ blocks) ->+            flip mapMaybe (zip [(0::Int)..] blocks) $ \case+                    (i, CodeBlock (_, cs, _) text)+                        | "brassica" `elem` cs -> Just $+                          singleTest (show i) $ DocTest (T.pack prefix) text+                    _ -> Nothing+    pure $ testGroup path tests -data DocTest = DocTest FilePath String+data DocTest = DocTest T.Text T.Text  instance IsTest DocTest where     testOptions = pure []-    run _ (DocTest path prefix) _ = do-        doc <- T.readFile path-        runIO (readMarkdown def { readerExtensions = githubMarkdownExtensions } doc) <&> \case-            Left err -> testFailed $ T.unpack $ renderError err-            Right (Pandoc _ blocks) ->-                let rules = flip mapMaybe blocks $ \case-                        CodeBlock (_, cs, _) text-                            | "brassica" `elem` cs-                            ->-                                let rule = prefix ++ T.unpack text-                                    examples = flip mapMaybe (T.lines text) $ \x ->-                                        case words $ T.unpack x of-                                            ";":i:"→":o:_ -> Just (i, o)-                                            _ -> Nothing-                                in Just (rule, examples)-                        _ -> Nothing-                    results = for rules $ \(rule, examples) ->-                        case parseSoundChanges rule of-                            Left err -> Left $ errorBundlePretty err-                            Right scs ->-                                case expandSoundChanges scs of-                                    Left err -> Left $ show err-                                    Right scs' -> for examples $ \(i, o) ->-                                        case withFirstCategoriesDecl tokeniseWord scs' i of-                                            Left err -> Left $ errorBundlePretty err-                                            Right i' ->-                                                let o' = mapMaybe getOutput $ applyChanges scs' i'-                                                    o'' =-                                                        if null o'-                                                        then "(deleted)"-                                                        else intercalate "/" $ concatWithBoundary <$> o'-                                                in if o == o''-                                                      then Right ("" :: String)-                                                      else Left $-                                                               "expected " ++ i ++ "→" ++ o ++-                                                               ", got " ++ o'' ++-                                                               "\nfor sound change:\n" ++ rule-                in case results of-                    Left err -> testFailed err-                    Right _ -> testPassed ""+    run _ (DocTest prefix text) _ = pure $+        let rule = T.unpack (prefix <> text)+            examples = flip mapMaybe (T.lines text) $ \x ->+                case words $ T.unpack x of+                    ";":i:"→":o:_ -> Just (i, o)+                    _ -> Nothing+            results = case parseSoundChanges rule of+                Left err -> Left $ errorBundlePretty err+                Right scs ->+                    case expandSoundChanges scs of+                        Left err -> Left $ show err+                        Right scs' -> for examples $ \(i, o) ->+                            case withFirstCategoriesDecl tokeniseWord scs' i of+                                Left err -> Left $ errorBundlePretty err+                                Right i' ->+                                    let o' = mapMaybe getOutput $ applyChanges scs' i'+                                        o'' =+                                            if null o'+                                            then "(deleted)"+                                            else intercalate "/" $ concatWithBoundary <$> o'+                                    in if o == o''+                                            then Right ("" :: String)+                                            else Left $+                                                    "expected " ++ i ++ "→" ++ o +++                                                    ", got " ++ o'' +++                                                    "\nfor sound change:\n" ++ rule+        in case results of+            Left err -> testFailed err+            Right _ -> testPassed ""