packages feed

phino 0.0.106 → 0.0.107

raw patch · 14 files changed

+405/−87 lines, 14 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ CLI.Helpers: printPhi :: PrintContext -> Expression -> String
+ CLI.Parsers: optHeaders :: Parser Bool
+ CLI.Parsers: optHideRho :: Parser Bool
+ CLI.Types: [_headers] :: OptsRewrite -> Bool
+ CLI.Types: [_hideRho] :: OptsRewrite -> Bool
+ LaTeX: [_headers] :: LatexContext -> Bool
+ Printer: printExpressionHidingRho' :: Expression -> PrintConfig -> String
+ Rewriter: stepHeaders :: [Rewritten] -> [String]
+ Sugar: withoutRho :: EXPRESSION -> EXPRESSION
- CLI.Types: OptsDataize :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> LineFormat -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Int -> Bool -> Bool -> Int -> Int -> Int -> Maybe Int -> Maybe Int -> [String] -> [String] -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Maybe FilePath -> Maybe FilePath -> OptsDataize
+ CLI.Types: OptsDataize :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> Bool -> LineFormat -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Int -> Bool -> Bool -> Int -> Int -> Int -> Maybe Int -> Maybe Int -> [String] -> [String] -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Maybe FilePath -> Maybe FilePath -> OptsDataize
- CLI.Types: OptsRewrite :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> LineFormat -> Must -> Bool -> Bool -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Int -> Int -> Int -> Maybe Int -> Maybe Int -> [FilePath] -> [String] -> [String] -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe FilePath -> Maybe FilePath -> Maybe FilePath -> OptsRewrite
+ CLI.Types: OptsRewrite :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> Bool -> LineFormat -> Must -> Bool -> Bool -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Int -> Int -> Int -> Maybe Int -> Maybe Int -> [FilePath] -> [String] -> [String] -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe FilePath -> Maybe FilePath -> Maybe FilePath -> OptsRewrite
- CLI.Types: PrintCtx :: SugarType -> LineFormat -> Int -> XmirContext -> Bool -> Bool -> Bool -> Bool -> Int -> Int -> Expression -> Maybe String -> Maybe String -> Maybe String -> IOFormat -> PrintContext
+ CLI.Types: PrintCtx :: SugarType -> Bool -> LineFormat -> Int -> XmirContext -> Bool -> Bool -> Bool -> Bool -> Bool -> Int -> Int -> Expression -> Maybe String -> Maybe String -> Maybe String -> IOFormat -> PrintContext
- LaTeX: LatexContext :: SugarType -> LineFormat -> Int -> Bool -> Bool -> Bool -> Int -> Int -> Expression -> Maybe String -> Maybe String -> Maybe String -> LatexContext
+ LaTeX: LatexContext :: SugarType -> LineFormat -> Int -> Bool -> Bool -> Bool -> Int -> Int -> Expression -> Maybe String -> Maybe String -> Maybe String -> Bool -> LatexContext

Files

README.md view
@@ -34,7 +34,7 @@  ```bash cabal update-cabal install --overwrite-policy=always phino-0.0.105+cabal install --overwrite-policy=always phino-0.0.106 phino --version ``` @@ -447,55 +447,55 @@ === parse/phi ===   warmup:     3 iterations   batches:    10 x 1-  total:      1289912.679 μs-  avg:        128991.268 μs-  min:        117209.849 μs-  max:        157967.879 μs-  std dev:    16178.452 μs+  total:      1279870.467 μs+  avg:        127987.047 μs+  min:        117457.988 μs+  max:        156105.148 μs+  std dev:    15737.223 μs === parse/xmir ===   warmup:     3 iterations   batches:    10 x 1-  total:      7732390.781 μs-  avg:        773239.078 μs-  min:        704582.049 μs-  max:        844495.931 μs-  std dev:    54136.286 μs+  total:      7617419.851 μs+  avg:        761741.985 μs+  min:        686228.460 μs+  max:        914941.984 μs+  std dev:    59800.299 μs === rewrite/normalize ===   warmup:     3 iterations   batches:    10 x 1-  total:      665998.989 μs-  avg:        66599.899 μs-  min:        63509.721 μs-  max:        72014.960 μs-  std dev:    2455.894 μs+  total:      603364.936 μs+  avg:        60336.494 μs+  min:        58688.247 μs+  max:        62431.021 μs+  std dev:    1196.183 μs === print/sweet/multiline ===   warmup:     3 iterations   batches:    10 x 1-  total:      4722211.658 μs-  avg:        472221.166 μs-  min:        463189.991 μs-  max:        485006.422 μs-  std dev:    6780.561 μs+  total:      4208011.890 μs+  avg:        420801.189 μs+  min:        400713.926 μs+  max:        442532.655 μs+  std dev:    16080.251 μs === print/sweet/flat ===   warmup:     3 iterations   batches:    10 x 1-  total:      4645473.928 μs-  avg:        464547.393 μs-  min:        431061.415 μs-  max:        488222.918 μs-  std dev:    21693.313 μs+  total:      4195034.891 μs+  avg:        419503.489 μs+  min:        403618.144 μs+  max:        432523.908 μs+  std dev:    9981.104 μs === print/salty/multiline ===   warmup:     3 iterations   batches:    10 x 1-  total:      14501593.398 μs-  avg:        1450159.340 μs-  min:        1407041.861 μs-  max:        1506159.801 μs-  std dev:    33325.764 μs+  total:      13849441.895 μs+  avg:        1384944.189 μs+  min:        1372281.323 μs+  max:        1410740.491 μs+  std dev:    10841.096 μs ```  The results were calculated in [this GHA job][benchmark-gha]-on 2026-07-07 at 18:32,+on 2026-07-24 at 23:39, on Linux with 4 CPUs.  <!-- benchmark_end -->@@ -544,4 +544,4 @@ [jna]: https://github.com/java-native-access/jna [jna-native]: https://github.com/java-native-access/jna/blob/master/src/com/sun/jna/Native.java [jeo]: https://github.com/objectionary/jeo-maven-plugin-[benchmark-gha]: https://github.com/objectionary/phino/actions/runs/28889268073+[benchmark-gha]: https://github.com/objectionary/phino/actions/runs/30134184768
phino.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: phino-version: 0.0.106+version: 0.0.107 license: MIT synopsis: Command-Line Manipulator of 𝜑-Calculus Expressions description: Please see the README on GitHub at <https://github.com/objectionary/phino#readme>
src/CLI/Helpers.hs view
@@ -25,7 +25,7 @@ import Parser (parseExpressionThrows) import qualified Printer as P import qualified Random as R-import Rewriter (Rewritten, Rewrittens')+import Rewriter (Rewritten, Rewrittens', stepHeaders) import System.IO (getContents') import Text.Printf (printf) import XMIR (expressionToXMIR, parseXMIRThrows, printXMIR, xmirToPhi)@@ -69,28 +69,48 @@ printRewrittens :: PrintContext -> Rewrittens' -> IO String printRewrittens ctx@PrintCtx{..} rewrittens@(chain, _)   | _outputFormat == LATEX && _sequence = rewrittensToLatex rewrittens (printCtxToLatexCtx ctx)-  | _focus == ExRoot = mapM (printInFormat ctx . fst) (canonized chain) <&> intercalate "\n"-  | otherwise = mapM (\(expr, _) -> locatedExpression _focus expr >>= printExpression ctx) (canonized chain) <&> intercalate "\n"+  | otherwise = withHeaders <$> mapM render (canonized chain)   where+    render :: Rewritten -> IO String+    render (expr, _)+      | _focus == ExRoot = printInFormat ctx expr+      | otherwise = locatedExpression _focus expr >>= printExpression ctx     canonized :: [Rewritten] -> [Rewritten]     canonized = if _canonize then canonize else id+    -- Prefix every step with an empty line and its header (see 'stepHeaders')+    -- when '--headers' is on. Headers, like the other intermediate-output+    -- flags, are meaningful only together with '--sequence'. Node counts come+    -- from the original 'chain', not the canonized one, since canonization+    -- only renames functions and never changes the AST size.+    withHeaders :: [String] -> String+    withHeaders rendered+      | _headers && _sequence = intercalate "\n" (zipWith prefixed (stepHeaders chain) rendered)+      | otherwise = intercalate "\n" rendered+      where+        prefixed :: String -> String -> String+        prefixed = printf "\n%s\n%s"  printExpression :: PrintContext -> Expression -> IO String printExpression ctx@PrintCtx{..} ex = case _outputFormat of-  PHI -> pure (P.printExpression' ex (_sugar, UNICODE, _line, _margin))+  PHI -> pure (printPhi ctx ex)   XMIR -> throwIO CouldNotPrintExpressionInXMIR   LATEX -> pure (expressionToLaTeX ex (printCtxToLatexCtx ctx))  -- Convert an expression to its corresponding String format printInFormat :: PrintContext -> Expression -> IO String printInFormat ctx@PrintCtx{..} expr = case _outputFormat of-  PHI -> pure (P.printExpression' expr (_sugar, UNICODE, _line, _margin))+  PHI -> pure (printPhi ctx expr)   XMIR -> expressionToXMIR expr _xmirCtx <&> printXMIR   LATEX -> pure (expressionToLaTeX expr (printCtxToLatexCtx ctx)) +-- Render an expression as PHI, dropping every ρ binding when '--hide-rho' is set.+printPhi :: PrintContext -> Expression -> String+printPhi PrintCtx{..} expr =+  (if _hideRho then P.printExpressionHidingRho' else P.printExpression') expr (_sugar, UNICODE, _line, _margin)+ printCtxToLatexCtx :: PrintContext -> LatexContext printCtxToLatexCtx PrintCtx{..} =-  LatexContext _sugar _line _margin _nonumber _compress _canonize _meetPopularity _meetLength _focus _expression _label _meetPrefix+  LatexContext _sugar _line _margin _nonumber _compress _canonize _meetPopularity _meetLength _focus _expression _label _meetPrefix _headers  -- Get rules for rewriting depending on provided flags getRules :: Bool -> Bool -> [FilePath] -> IO [Y.Rule]
src/CLI/Parsers.hs view
@@ -121,6 +121,13 @@ optSequence :: Parser Bool optSequence = switch (long "sequence" <> help "Result output contains all intermediate 𝜑-expressions concatenated with EOL") +optHeaders :: Parser Bool+optHeaders =+  switch+    ( long "headers"+        <> help "Prefix every intermediate step (see --sequence) with a header line: step number, the rule that produced it, and AST node count before and after that rule"+    )+ optCanonize :: Parser Bool optCanonize = switch (long "canonize" <> help "Rename all functions attached to λ binding with Fn1, Fn2, etc.") @@ -206,6 +213,9 @@ optSugar :: Parser SugarType optSugar = flag SALTY SWEET (long "sweet" <> help (printf "Print result and intermediate (see %s option(s)) 𝜑-expressions using syntax sugar" _intermediateOptions)) +optHideRho :: Parser Bool+optHideRho = switch (long "hide-rho" <> help "Remove every ρ binding from result and intermediate 𝜑-expressions for cleaner output")+ optSugar' :: Parser SugarType optSugar' = flag SALTY SWEET (long "sweet" <> help "Print result 𝜑-expression using syntax sugar") @@ -262,11 +272,13 @@             <*> optInputFormat             <*> optOutputFormat             <*> optSugar+            <*> optHideRho             <*> optLineFormat             <*> optOmitListing             <*> optOmitComments             <*> optNonumber             <*> optSequence+            <*> optHeaders             <*> optCanonize             <*> optDepthSensitive             <*> optShuffle@@ -298,6 +310,7 @@             <*> optInputFormat             <*> optOutputFormat             <*> optSugar+            <*> optHideRho             <*> optLineFormat             <*> optMust             <*> optNormalize@@ -310,6 +323,7 @@             <*> switch (long "in-place" <> help "Edit file in-place instead of printing to output")             <*> switch (long "update" <> help "Skip rewriting if --target file is newer than the input file")             <*> optSequence+            <*> optHeaders             <*> optCanonize             <*> optCompress             <*> optMaxDepth
src/CLI/Runners.hs view
@@ -119,6 +119,7 @@     toPrintCtx xmirCtx focus =       PrintCtx         _sugarType+        _hideRho         _flat         _margin         xmirCtx@@ -126,6 +127,7 @@         _compress         _canonize         _sequence+        _headers         (justMeetPopularity _meetPopularity)         (justMeetLength _meetLength)         focus@@ -167,6 +169,7 @@     toPrintCtx focus =       PrintCtx         _sugarType+        _hideRho         _flat         _margin         defaultXmirContext@@ -174,6 +177,7 @@         _compress         _canonize         _sequence+        _headers         (justMeetPopularity _meetPopularity)         (justMeetLength _meetLength)         focus@@ -219,9 +223,11 @@     toPrintCtx xmirCtx =       PrintCtx         _sugarType+        False         _flat         _margin         xmirCtx+        False         False         False         False
src/CLI/Types.hs view
@@ -17,6 +17,7 @@  data PrintContext = PrintCtx   { _sugar :: SugarType+  , _hideRho :: Bool   , _line :: LineFormat   , _margin :: Int   , _xmirCtx :: XmirContext@@ -24,6 +25,7 @@   , _compress :: Bool   , _canonize :: Bool   , _sequence :: Bool+  , _headers :: Bool   , _meetPopularity :: Int   , _meetLength :: Int   , _focus :: Expression@@ -77,11 +79,13 @@   , _inputFormat :: IOFormat   , _outputFormat :: IOFormat   , _sugarType :: SugarType+  , _hideRho :: Bool   , _flat :: LineFormat   , _omitListing :: Bool   , _omitComments :: Bool   , _nonumber :: Bool   , _sequence :: Bool+  , _headers :: Bool   , _canonize :: Bool   , _depthSensitive :: Bool   , _shuffle :: Bool@@ -122,6 +126,7 @@   , _inputFormat :: IOFormat   , _outputFormat :: IOFormat   , _sugarType :: SugarType+  , _hideRho :: Bool   , _flat :: LineFormat   , _must :: Must   , _normalize :: Bool@@ -134,6 +139,7 @@   , _inPlace :: Bool   , _update :: Bool   , _sequence :: Bool+  , _headers :: Bool   , _canonize :: Bool   , _compress :: Bool   , _maxDepth :: Int
src/LaTeX.hs view
@@ -38,7 +38,7 @@ import Misc import Render (Render (render)) import Replacer (replaceExpression)-import Rewriter (Rewritten, Rewrittens')+import Rewriter (Rewritten, Rewrittens', stepHeaders) import Sugar (SugarType (SWEET), ToSalty, withSugarType) import Text.Printf (printf) import Text.Read (readMaybe)@@ -57,10 +57,11 @@   , _expression :: Maybe String   , _label :: Maybe String   , _meetPrefix :: Maybe String+  , _headers :: Bool   }  defaultLatexContext :: LatexContext-defaultLatexContext = LatexContext SWEET SINGLELINE defaultMargin False False False defaultMeetPopularity defaultMeetLength ExRoot Nothing Nothing Nothing+defaultLatexContext = LatexContext SWEET SINGLELINE defaultMargin False False False defaultMeetPopularity defaultMeetLength ExRoot Nothing Nothing Nothing False  defaultMeetPopularity :: Int defaultMeetPopularity = 50@@ -158,16 +159,21 @@ -- than 0 (via 'baseTab'); this keeps a wrapped multi-line step's members nested -- one level below its '\leadsto [[' line and its closing bracket aligned with -- that line. The first step has no '\leadsto' prefix and stays at base tab 0.-body :: [(a, Maybe String)] -> (Int -> a -> String) -> String-body printed toLatex =+-- Each step is prefixed with the matching entry from 'comments', which is+-- either empty or a '% ...'-commented header line ending in a newline (see+-- 'stepComments'), so headers stay on their own line above the equation.+body :: [String] -> [(a, Maybe String)] -> (Int -> a -> String) -> String+body comments printed toLatex =   intercalate-    "\n  \\leadsto "-    ( zipWith-        ( \idx (item, maybeName) ->+    "\n"+    ( zipWith3+        ( \idx comment (item, maybeName) ->             let item' = toLatex (baseTab idx) item-             in maybe item' (printf "%s \\leadsto_{\\nameref{r:%s}}" item') maybeName+                leadsto = if idx == 0 then item' else "  \\leadsto " ++ item'+             in comment ++ maybe leadsto (printf "%s \\leadsto_{\\nameref{r:%s}}" leadsto) maybeName         )         [0 ..]+        comments         printed     )   where@@ -175,6 +181,17 @@     baseTab 0 = 0     baseTab _ = 1 +-- LaTeX comment header lines for each step (see 'stepHeaders' in "Rewriter"),+-- or empty strings when '--headers' is off. A '%' starts a LaTeX comment, so+-- the header documents the '--sequence' chain without affecting the rendered+-- equation. Each comment ends in a newline so the following step starts on a+-- fresh line.+stepComments :: [Rewritten] -> LatexContext -> [String]+stepComments rewrittens LatexContext{_headers = enabled} =+  if enabled+    then map (printf "%% %s\n") (stepHeaders rewrittens)+    else map (const "") rewrittens+ ending :: Bool -> LatexContext -> String ending True ctx = printf " \\leadsto\n  \\leadsto \\dots\n\\end{%s}" (phiquation ctx) ending False ctx = printf "{.}\n\\end{%s}" (phiquation ctx)@@ -212,7 +229,7 @@   pure     ( concat         [ preamble ctx-        , body (canonizedRewrittens (compressedRewrittens rewrittens ctx) ctx) (\tabs expr -> renderToLatex (expressionToCSTFrom tabs expr) ctx)+        , body (stepComments rewrittens ctx) (canonizedRewrittens (compressedRewrittens rewrittens ctx) ctx) (\tabs expr -> renderToLatex (expressionToCSTFrom tabs expr) ctx)         , ending exceeded ctx         ]     )@@ -222,7 +239,7 @@   pure     ( concat         [ preamble ctx-        , body (zip (canonizedExpressions (compressedExpressions focused ctx) ctx) rules) (\tabs expr -> renderToLatex (expressionToCSTFrom tabs expr) ctx)+        , body (stepComments rewrittens ctx) (zip (canonizedExpressions (compressedExpressions focused ctx) ctx) rules) (\tabs expr -> renderToLatex (expressionToCSTFrom tabs expr) ctx)         , ending exceeded ctx         ]     )
src/Parser.hs view
@@ -82,15 +82,19 @@   return (T.pack (first : rest))  function :: Parser String-function = lexeme $ do-  first <- oneOf ['A' .. 'Z']-  rest <--    many-      ( satisfy-          (\ch -> isDigit ch || isAsciiLower ch || ch == '_' || ch == 'φ')-          <?> "allowed character in function name"-      )-  return (first : rest)+function =+  lexeme+    ( do+        first <- oneOf ['A' .. 'Z']+        rest <-+          many+            ( satisfy+                (\ch -> isDigit ch || isAsciiLower ch || ch == '_' || ch == 'φ')+                <?> "allowed character in function name"+            )+        return (first : rest)+    )+    <?> "function name"  delta :: Parser String delta =@@ -244,7 +248,7 @@ tauValue :: Parser Expression tauValue =   choice-    [ try $ do+    [ do         _ <- arrow         expression     , do@@ -265,41 +269,42 @@     rb :: Parser String     rb = symbol ")" -tauBinding :: Parser Attribute -> Parser Binding-tauBinding attr = BiTau <$> attr <*> tauValue- metaBinding :: Parser Binding metaBinding = BiMeta <$> meta' 'B' "𝐵"  -- binding--- 1. tau--- 2. void--- 3. delta--- 4. meta delta--- 5. meta--- 6. lambda--- 7. meta lambda+-- 1. delta+-- 2. meta delta+-- 3. meta+-- 4. lambda+-- 5. meta lambda+-- 6. void+-- 7. tau+--+-- Every alternative commits as soon as the token that tells it apart from its+-- siblings is consumed, so a failure deeper in the binding keeps its own+-- position instead of being rewound to the beginning of the binding. binding :: Parser Binding binding =   choice-    [ try (tauBinding attribute)-    , try $ do-        attr <- attribute-        _ <- arrow-        _ <- choice [symbol "?", symbol "∅"]-        return (BiVoid attr)-    , try $ do-        _ <- delta+    [ do+        _ <- try delta         BiDelta <$> bytes     , try metaBinding-    , try $ do-        _ <- lambda-        BiLambda . Function . T.pack <$> function     , do-        _ <- lambda-        BiLambda . FnMeta <$> meta' 'F' "𝑓"+        _ <- try lambda+        BiLambda <$> choice [Function . T.pack <$> function, FnMeta <$> meta' 'F' "𝑓"]+    , do+        attr <- attribute+        choice+          [ try blank >> return (BiVoid attr)+          , BiTau attr <$> tauValue+          ]     ]     <?> "binding"+  where+    blank :: Parser String+    blank = arrow >> choice [symbol "?", symbol "∅"]  -- inlined void attribute -- 1. label@@ -352,7 +357,7 @@ argument :: Parser Argument argument =   choice-    [ try (ArAlpha <$> alpha <*> tauValue)+    [ ArAlpha <$> try alpha <*> tauValue     , ArTau <$> attribute <*> tauValue     ]     <?> "argument"
src/Printer.hs view
@@ -7,6 +7,7 @@ module Printer   ( printExpression   , printExpression'+  , printExpressionHidingRho'   , printAttribute   , printAlpha   , printBinding@@ -42,7 +43,18 @@ logPrintConfig = (SWEET, UNICODE, SINGLELINE, defaultMargin)  printExpression' :: Expression -> PrintConfig -> String-printExpression' ex (sugar, encoding, line, margin) = T.unpack $ render (withLineFormat line $ withMargin margin $ withEncoding encoding $ withSugarType sugar $ expressionToCST ex)+printExpression' = printExpressionWith id++-- Like 'printExpression'', but drops every ρ binding from the rendered+-- expression (the '--hide-rho' switch). See 'withoutRho'.+printExpressionHidingRho' :: Expression -> PrintConfig -> String+printExpressionHidingRho' = printExpressionWith withoutRho++-- Shared rendering pipeline with a hook applied to the sugared CST, right+-- before encoding and margin wrapping.+printExpressionWith :: (EXPRESSION -> EXPRESSION) -> Expression -> PrintConfig -> String+printExpressionWith hide ex (sugar, encoding, line, margin) =+  T.unpack $ render (withLineFormat line $ withMargin margin $ withEncoding encoding $ hide $ withSugarType sugar $ expressionToCST ex)  printExpression :: Expression -> String printExpression ex = printExpression' ex defaultPrintConfig
src/Rewriter.hs view
@@ -9,7 +9,7 @@ -- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com -- SPDX-License-Identifier: MIT -module Rewriter (rewrite, RewriteContext (..), Rewritten, Rewrittens, Rewrittens') where+module Rewriter (rewrite, RewriteContext (..), Rewritten, Rewrittens, Rewrittens', stepHeaders) where  import AST import Builder@@ -17,6 +17,7 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe) import Deps import Locator (locatedExpression, withLocatedExpression) import Logger (logDebug)@@ -53,6 +54,27 @@ type Rewrittens = (NonEmpty Rewritten, Bool)  type Rewrittens' = ([Rewritten], Bool)++-- Build a header line for every step of a rewriting chain. The chain is+-- '[(e0, Just r0), ..., (en, Nothing)]', where 'ri' is the rule applied to 'ei'+-- to produce 'e(i+1)' (see 'leadsTo'). A step's header names the rule that+-- produced its expression, together with the AST node counts before and after+-- that rule, e.g. "=== Step #4, Rule 'STOP', 32t -> 43t". The very first step is+-- the input, which no rule produced, so it carries only its number:+-- "=== Step #1". The 'N nodes -> M nodes' pair matches the debug log emitted+-- while rewriting.+stepHeaders :: [Rewritten] -> [String]+stepHeaders chain = zipWith3 header [1 ..] chain (Nothing : map Just chain)+  where+    header :: Int -> Rewritten -> Maybe Rewritten -> String+    header step _ Nothing = printf "=== Step #%d" step+    header step (current, _) (Just (before, rule)) =+      printf+        "=== Step #%d, Rule '%s', %dt -> %dt"+        step+        (fromMaybe "?" rule)+        (countNodes before)+        (countNodes current)  type ToReplace = (Expression, Expression, Expression, [Subst]) 
src/Sugar.hs view
@@ -7,7 +7,7 @@ -- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com -- SPDX-License-Identifier: MIT -module Sugar (toSalty, withSugarType, SugarType (..), ToSalty) where+module Sugar (toSalty, withSugarType, withoutRho, SugarType (..), ToSalty) where  import AST import Bytes (numToBts, strToBts)@@ -39,6 +39,75 @@  data SugarType = SWEET | SALTY   deriving (Eq, Show)++-- Drop every ρ binding (ρ ↦ ∅, ρ ↦ e and ρ(…) ↦ e) from a rendered CST, the+-- effect of the '--hide-rho' switch. It runs after 'withSugarType', so it also+-- removes the ρ ↦ ∅ that 'bdWithVoidRho' re-inserts into every formation on the+-- SALTY path. Only formation bindings are stripped: dispatches such as ξ.ρ and+-- application arguments are left untouched, and a formation left empty by the+-- strip collapses to the compact '⟦⟧' layout.+withoutRho :: EXPRESSION -> EXPRESSION+withoutRho = goExpr+  where+    goExpr :: EXPRESSION -> EXPRESSION+    goExpr EX_FORMATION{..} = case goBinding binding of+      empty@BI_EMPTY{} -> EX_FORMATION lsb NO_EOL NO_TAB empty NO_EOL NO_TAB rsb+      binding' -> EX_FORMATION lsb eol tab binding' eol' tab' rsb+    goExpr EX_DISPATCH{..} = EX_DISPATCH (goExpr expr) space attr+    goExpr EX_APPLICATION{..} = EX_APPLICATION (goExpr expr) space eol tab (goArgument argument) eol' tab' indent+    goExpr EX_PHI_MEET{..} = EX_PHI_MEET prefix idx (goExpr expr)+    goExpr EX_PHI_AGAIN{..} = EX_PHI_AGAIN prefix idx (goExpr expr)+    goExpr expr = expr+    -- Formation bindings: drop the ρ pairs, recurse into whatever remains.+    goBinding :: BINDING -> BINDING+    goBinding empty@BI_EMPTY{} = empty+    goBinding BI_META{..} = BI_META meta (goBindings bindings) tab+    goBinding BI_PAIR{..}+      | isRho pair = promote tab (goBindings bindings)+      | otherwise = BI_PAIR (goPair pair) (goBindings bindings) tab+    goBindings :: BINDINGS -> BINDINGS+    goBindings empty@BDS_EMPTY{} = empty+    goBindings BDS_META{..} = BDS_META eol tab meta (goBindings bindings)+    goBindings BDS_PAIR{..}+      | isRho pair = goBindings bindings+      | otherwise = BDS_PAIR eol tab (goPair pair) (goBindings bindings)+    -- Turn the tail chain back into a head binding once its leading pair was+    -- dropped; the promoted pair is already stripped and recursed by 'goBindings'.+    promote :: TAB -> BINDINGS -> BINDING+    promote tab (BDS_EMPTY _) = BI_EMPTY tab+    promote tab (BDS_PAIR _ _ pair bindings) = BI_PAIR pair bindings tab+    promote tab (BDS_META _ _ meta bindings) = BI_META meta bindings tab+    -- Application arguments keep their ρ (bdWithVoidRho never adds one there);+    -- only recurse into the expressions they carry.+    goArgument :: APP_ARGUMENT -> APP_ARGUMENT+    goArgument (AA_TAU binding) = AA_TAU (goAppBinding binding)+    goArgument (AA_TAUS binding) = AA_TAUS (goArgBinding binding)+    goArgument (AA_EXPRS args) = AA_EXPRS (goAppArg args)+    goAppBinding :: APP_BINDING -> APP_BINDING+    goAppBinding APP_BINDING{..} = APP_BINDING (goPair pair)+    goArgBinding :: BINDING -> BINDING+    goArgBinding empty@BI_EMPTY{} = empty+    goArgBinding BI_META{..} = BI_META meta (goArgBindings bindings) tab+    goArgBinding BI_PAIR{..} = BI_PAIR (goPair pair) (goArgBindings bindings) tab+    goArgBindings :: BINDINGS -> BINDINGS+    goArgBindings empty@BDS_EMPTY{} = empty+    goArgBindings BDS_META{..} = BDS_META eol tab meta (goArgBindings bindings)+    goArgBindings BDS_PAIR{..} = BDS_PAIR eol tab (goPair pair) (goArgBindings bindings)+    goAppArg :: APP_ARG -> APP_ARG+    goAppArg APP_ARG{..} = APP_ARG (goExpr expr) (goAppArgs args)+    goAppArgs :: APP_ARGS -> APP_ARGS+    goAppArgs AAS_EMPTY = AAS_EMPTY+    goAppArgs AAS_EXPR{..} = AAS_EXPR eol tab (goExpr expr) (goAppArgs args)+    goPair :: PAIR -> PAIR+    goPair PA_TAU{..} = PA_TAU attr arrow (goExpr expr)+    goPair PA_ALPHA{..} = PA_ALPHA alpha arrow (goExpr expr)+    goPair PA_FORMATION{..} = PA_FORMATION attr voids arrow (goExpr expr)+    goPair pair = pair+    isRho :: PAIR -> Bool+    isRho PA_VOID{attr = AT_RHO _} = True+    isRho PA_TAU{attr = AT_RHO _} = True+    isRho PA_FORMATION{attr = AT_RHO _} = True+    isRho _ = False  -- By default CST is generated with all possible syntax sugar -- The main purpose of this class is to get rid of syntax sugar
test/CLISpec.hs view
@@ -137,6 +137,25 @@           ["--pin=", "rewrite"]           ["Version mismatch: --pin requires ''"] +  describe "--hide-rho" $ do+    it "drops every rho binding from the default salty output" $+      withStdin "[[ foo -> [[ x -> [[ ]], ^ -> $.y ]], y -> [[ ]] ]]" $+        testCLISucceeded+          ["rewrite", "--flat", "--hide-rho"]+          ["⟦ foo ↦ ⟦ x ↦ ⟦⟧ ⟧, y ↦ ⟦⟧ ⟧"]++    it "also drops the rho that --sweet leaves behind" $+      withStdin "[[ foo -> [[ x -> [[ ]], ^ -> $.y ]], y -> [[ ]] ]]" $+        testCLISucceeded+          ["rewrite", "--flat", "--sweet", "--hide-rho"]+          ["⟦ foo ↦ ⟦ x ↦ ⟦⟧ ⟧, y ↦ ⟦⟧ ⟧"]++    it "keeps sweet numeric literals intact" $+      withStdin "[[ a -> 42 ]]" $+        testCLISucceeded+          ["rewrite", "--flat", "--sweet", "--hide-rho"]+          ["⟦ a ↦ 42 ⟧"]+   it "prints debug info with --log-level=DEBUG" $     withStdin "[[]]" $       testCLISucceeded ["rewrite", "--log-level=DEBUG"] ["[DEBUG]:"]@@ -542,6 +561,65 @@               [ "⟦ x ↦ \"foo\" ⟧"               , "Φ.x( y ↦ \"foo\" )"               , "⟦ x ↦ \"foo\" ⟧"+              ]+          ]++    it "prefixes every step with a header when --headers is on" $+      withStdin "[[ x -> \"foo\" ]]" $+        testCLISucceeded+          [ "rewrite"+          , rule "first.yaml"+          , rule "second.yaml"+          , "--max-depth=1"+          , "--max-cycles=2"+          , "--sequence"+          , "--headers"+          , "--sweet"+          , "--flat"+          ]+          [ intercalate+              "\n"+              [ ""+              , "=== Step #1"+              , "⟦ x ↦ \"foo\" ⟧"+              , ""+              , "=== Step #2, Rule 'first', 31t -> 30t"+              , "Φ.x( y ↦ \"foo\" )"+              , ""+              , "=== Step #3, Rule 'second', 30t -> 31t"+              , "⟦ x ↦ \"foo\" ⟧"+              ]+          ]++    it "ignores --headers without --sequence" $+      withStdin "[[ x -> \"foo\" ]]" $+        testCLISucceeded+          ["rewrite", rule "simple.yaml", "--headers", "--sweet", "--flat"]+          ["⟦ x ↦ \"bar\" ⟧"]++    it "emits step headers as LaTeX comments with --headers" $+      withStdin "[[ x -> \"foo\" ]]" $+        testCLISucceeded+          [ "rewrite"+          , rule "first.yaml"+          , rule "second.yaml"+          , "--max-depth=1"+          , "--max-cycles=2"+          , "--sequence"+          , "--headers"+          , "--sweet"+          , "--flat"+          , "--output=latex"+          ]+          [ unlines+              [ "\\begin{phiquation}"+              , "% === Step #1"+              , "[[ |x| -> \"foo\" ]] \\leadsto_{\\nameref{r:first}}"+              , "% === Step #2, Rule 'first', 31t -> 30t"+              , "  \\leadsto Q . |x| ( |y| -> \"foo\" ) \\leadsto_{\\nameref{r:second}}"+              , "% === Step #3, Rule 'second', 30t -> 31t"+              , "  \\leadsto [[ |x| -> \"foo\" ]]{.}"+              , "\\end{phiquation}"               ]           ] 
test/ParserSpec.hs view
@@ -10,6 +10,7 @@ import AST import Control.Monad (forM_) import Data.Either (isLeft, isRight)+import Data.List (isInfixOf) import Files (allPathsIn) import Parser import System.FilePath (takeBaseName)@@ -26,6 +27,15 @@       Just right -> function ipt `shouldBe` Right right       _ -> function ipt `shouldSatisfy` isLeft +fails ::+  (Show a) =>+  (String -> Either String a) ->+  [(String, String)] ->+  SpecWith (Arg Expectation)+fails function useCases =+  forM_ useCases $ \(ipt, fragment) ->+    it ipt (function ipt `shouldSatisfy` either (isInfixOf fragment) (const False))+ spec :: Spec spec = do   describe "parse expression" $@@ -245,6 +255,25 @@           , "⟦ k ↦ ⟦ Δ ⤍ 42-, Δ ⤍ 55- ⟧ ⟧"           ]       )++  describe "points at the typo instead of the beginning of the binding" $+    fails+      parseExpression+      [ ("[[ D> x ]]", "expression:1:7:")+      , ("[[ L> 42 ]]", "expression:1:7:")+      , ("⟦ a ↦ Φ.foo() ⟧", "expression:1:13:")+      , ("[[ x -> ]]", "expression:1:9:")+      , ("[[ x -> Q.y(] ]]", "expression:1:13:")+      , ("[[ y -> 5, x -> Q.z(} ]]", "expression:1:21:")+      ]++  describe "tells what a binding prefix may be followed by" $+    fails+      parseExpression+      [ ("[[ D> x ]]", "expecting bytes")+      , ("[[ L> 42 ]]", "function name")+      , ("[[ x -> ]]", "expecting '?', '∅', or expression head")+      ]    describe "parse packs" $ do     packs <- runIO (allPathsIn "test-resources/parser-packs")
test/PrinterSpec.hs view
@@ -122,6 +122,46 @@           it desc (printExpression' expr (SALTY, UNICODE, SINGLELINE, defaultMargin) `shouldBe` expected)       ) +  describe "printExpressionHidingRho strips every rho binding for --hide-rho" $ do+    let issueExpr =+          ExFormation+            [ BiTau+                (AtLabel "foo")+                ( ExFormation+                    [ BiTau (AtLabel "x") (ExFormation [BiVoid AtRho])+                    , BiTau AtRho (ExDispatch ExXi (AtLabel "y"))+                    ]+                )+            , BiTau (AtLabel "y") (ExFormation [BiVoid AtRho])+            , BiVoid AtRho+            ]+    forM_+      [ ("salty clears both void and dispatch-valued rho", SALTY, issueExpr, "⟦ foo ↦ ⟦ x ↦ ⟦⟧ ⟧, y ↦ ⟦⟧ ⟧")+      , ("sweet also drops the rho that --sweet keeps", SWEET, issueExpr, "⟦ foo ↦ ⟦ x ↦ ⟦⟧ ⟧, y ↦ ⟦⟧ ⟧")+      ,+        ( "a rho bound to an expression is removed"+        , SWEET+        , ExFormation [BiTau (AtLabel "a") ExRoot, BiTau AtRho (ExDispatch ExXi (AtLabel "y"))]+        , "⟦ a ↦ Φ ⟧"+        )+      , ("a formation holding only rho collapses to empty", SALTY, ExFormation [BiVoid AtRho], "⟦⟧")+      ,+        ( "a ξ.ρ dispatch value is left untouched"+        , SALTY+        , ExFormation [BiTau (AtLabel "a") (ExDispatch ExXi AtRho)]+        , "⟦ a ↦ ξ.ρ ⟧"+        )+      ]+      ( \(desc, sugar, expr, expected) ->+          it desc (printExpressionHidingRho' expr (sugar, UNICODE, SINGLELINE, defaultMargin) `shouldBe` expected)+      )++  describe "printExpressionHidingRho keeps primitives that carry no visible rho" $+    it "renders a sweet numeric literal exactly as printExpression' does" $ do+      let number = DataNumber (BtMany ["40", "45", "00", "00", "00", "00", "00", "00"])+          config = (SWEET, UNICODE, SINGLELINE, defaultMargin)+      printExpressionHidingRho' number config `shouldBe` printExpression' number config+   describe "printAttribute with default encoding" $     forM_       [ ("label", AtLabel "attr", "attr")