packages feed

phino 0.0.0.40 → 0.0.0.41

raw patch · 26 files changed

+1092/−406 lines, 26 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Builder: [_meta] :: BuildException -> String
- Pretty: instance Prettyprinter.Internal.Pretty Ast.Attribute
- Rewriter: instance GHC.Exception.Type.Exception Rewriter.MustException
- Rewriter: instance GHC.Show.Show Rewriter.MustException
+ Ast: instance GHC.Classes.Ord Ast.Attribute
+ Builder: [_msg] :: BuildException -> String
+ Dataize: [_depthSensitive] :: DataizeContext -> Bool
+ LaTeX: explainRules :: [Rule] -> String
+ LaTeX: programToLaTeX :: Program -> PrintMode -> String
+ Misc: attributesFromBindings :: [Binding] -> [Attribute]
+ Misc: uniqueBindings :: [Binding] -> Either String [Binding]
+ Misc: uniqueBindings' :: [Binding] -> IO [Binding]
+ Misc: validateYamlObject :: (Applicative a, MonadFail a) => Object -> [String] -> a ()
+ Must: MtDisabled :: Must
+ Must: MtExact :: Integer -> Must
+ Must: MtRange :: Maybe Integer -> Maybe Integer -> Must
+ Must: data Must
+ Must: exceedsUpperBound :: Must -> Integer -> Bool
+ Must: inRange :: Must -> Integer -> Bool
+ Must: instance GHC.Classes.Eq Must.Must
+ Must: instance GHC.Read.Read Must.Must
+ Must: instance GHC.Show.Show Must.Must
+ Pretty: ASCII :: Encoding
+ Pretty: UNICODE :: Encoding
+ Pretty: data Encoding
+ Pretty: instance Prettyprinter.Internal.Pretty (Pretty.Formatted Ast.Attribute)
+ Rewriter: [_depthSensitive] :: RewriteContext -> Bool
+ Rewriter: instance GHC.Exception.Type.Exception Rewriter.RewriteException
+ Rewriter: instance GHC.Show.Show Rewriter.RewriteException
+ Term: TeBindings :: [Binding] -> Term
+ XMIR: defaultXmirContext :: XmirContext
- Builder: buildAttribute :: Attribute -> Subst -> Either String Attribute
+ Builder: buildAttribute :: Attribute -> Subst -> Built Attribute
- Builder: buildBinding :: Binding -> Subst -> Either String [Binding]
+ Builder: buildBinding :: Binding -> Subst -> Built [Binding]
- Builder: buildBytes :: Bytes -> Subst -> Either String Bytes
+ Builder: buildBytes :: Bytes -> Subst -> Built Bytes
- Builder: buildExpression :: Expression -> Subst -> Either String (Expression, Expression)
+ Builder: buildExpression :: Expression -> Subst -> Built (Expression, Expression)
- Dataize: DataizeContext :: Program -> Integer -> Integer -> BuildTermFunc -> DataizeContext
+ Dataize: DataizeContext :: Program -> Integer -> Integer -> Bool -> BuildTermFunc -> DataizeContext
- Pretty: prettyProgram' :: Program -> PrintMode -> String
+ Pretty: prettyProgram' :: Program -> PrintMode -> Encoding -> String
- Pretty: prettySubsts' :: [Subst] -> PrintMode -> String
+ Pretty: prettySubsts' :: [Subst] -> PrintMode -> Encoding -> String
- Rewriter: RewriteContext :: Program -> Integer -> Integer -> BuildTermFunc -> Integer -> RewriteContext
+ Rewriter: RewriteContext :: Program -> Integer -> Integer -> Bool -> BuildTermFunc -> Must -> RewriteContext
- Rewriter: [_must] :: RewriteContext -> Integer
+ Rewriter: [_must] :: RewriteContext -> Must
- XMIR: XmirContext :: Bool -> Bool -> PrintMode -> XmirContext
+ XMIR: XmirContext :: Bool -> Bool -> String -> XmirContext

Files

phino.cabal view
@@ -1,7 +1,7 @@ cabal-version:      3.0  name:               phino-version:            0.0.0.40+version:            0.0.0.41 license:            MIT synopsis:           Command-Line Manipulator of 𝜑-Calculus Expressions description:        Please see the README on GitHub at <https://github.com/objectionary/phino#readme>@@ -37,11 +37,13 @@     Functions,     Term,     Misc,+    Must,     Logger,     Regexp,     Pretty,     Random,-    XMIR+    XMIR,+    LaTeX   hs-source-dirs: src   other-modules:     Paths_phino@@ -102,6 +104,7 @@     MiscSpec,     XMIRSpec,     DataizeSpec,+    FunctionsSpec,     Paths_phino   autogen-modules:     Paths_phino
src/Ast.hs view
@@ -47,7 +47,16 @@   | AtLambda -- λ, used only in yaml conditions   | AtDelta -- Δ, used only in yaml conditions   | AtMeta String -- !a-  deriving (Eq, Show, Generic)+  deriving (Eq, Generic, Ord)++instance Show Attribute where+  show (AtLabel label) = label+  show (AtAlpha idx) = 'α' : show idx+  show AtRho = "ρ"+  show AtPhi = "φ"+  show AtDelta = "Δ"+  show AtLambda = "λ"+  show (AtMeta meta) = '!' : meta  countNodes :: Program -> Integer countNodes (Program expr) = countNodes' expr
src/Builder.hs view
@@ -26,20 +26,23 @@ import Control.Exception (Exception, throwIO) import qualified Data.Map.Strict as Map import Matcher-import Pretty (prettyAttribute, prettyBinding, prettyExpression, prettySubst, prettyBytes)+import Misc (uniqueBindings)+import Pretty (prettyAttribute, prettyBinding, prettyBytes, prettyExpression, prettySubst) import Text.Printf (printf) import Yaml (ExtraArgument (..))  data BuildException-  = CouldNotBuildExpression {_expr :: Expression, _meta :: String}-  | CouldNotBuildAttribute {_attr :: Attribute, _meta :: String}-  | CouldNotBuildBinding {_bd :: Binding, _meta :: String}-  | CouldNotBuildBytes {_bts :: Bytes, _meta :: String}+  = CouldNotBuildExpression {_expr :: Expression, _msg :: String}+  | CouldNotBuildAttribute {_attr :: Attribute, _msg :: String}+  | CouldNotBuildBinding {_bd :: Binding, _msg :: String}+  | CouldNotBuildBytes {_bts :: Bytes, _msg :: String}   deriving (Exception)  metaMsg :: String -> String metaMsg = printf "meta '%s' is either does not exist or refers to an inappropriate term" +type Built a = Either String a+ -- @todo #277:30min Error messages are too verbose. Now, if we can't build expression or binding, we --  throw an exception and just print whole expression or binding to console. --  If this elements are big, it's just a mess and error message became unreadable. It would be nice to@@ -48,23 +51,23 @@   show CouldNotBuildExpression {..} =     printf       "Couldn't build expression, %s\n--Expression: %s"-      (metaMsg _meta)+      _msg       (prettyExpression _expr)   show CouldNotBuildAttribute {..} =     printf       "Couldn't build attribute '%s', %s"       (prettyAttribute _attr)-      (metaMsg _meta)+      _msg   show CouldNotBuildBinding {..} =     printf       "Couldn't build binding, %s\n--Binding: %s"-      (metaMsg _meta)+      _msg       (prettyBinding _bd)   show CouldNotBuildBytes {..} =     printf       "Couldn't build bytes '%s', %s"       (prettyBytes _bts)-      (metaMsg _meta)+      _msg  contextualize :: Expression -> Expression -> Program -> Expression contextualize ExGlobal _ (Program expr) = expr@@ -77,22 +80,22 @@       bexpr' = contextualize bexpr context prog    in ExApplication expr' (BiTau attr bexpr') -buildAttribute :: Attribute -> Subst -> Either String Attribute+buildAttribute :: Attribute -> Subst -> Built Attribute buildAttribute (AtMeta meta) (Subst mp) = case Map.lookup meta mp of   Just (MvAttribute attr) -> Right attr-  _ -> Left meta+  _ -> Left (metaMsg meta) buildAttribute attr _ = Right attr -buildBytes :: Bytes -> Subst -> Either String Bytes+buildBytes :: Bytes -> Subst -> Built Bytes buildBytes (BtMeta meta) (Subst mp) = case Map.lookup meta mp of   Just (MvBytes bytes) -> Right bytes-  _ -> Left meta+  _ -> Left (metaMsg meta) buildBytes bts _ = Right bts  -- Build binding -- The function returns [Binding] because the BiMeta is always attached -- to the list of bindings-buildBinding :: Binding -> Subst -> Either String [Binding]+buildBinding :: Binding -> Subst -> Built [Binding] buildBinding (BiTau attr expr) subst = do   attribute <- buildAttribute attr subst   (expression, _) <- buildExpression expr subst@@ -101,18 +104,18 @@   attribute <- buildAttribute attr subst   Right [BiVoid attribute] buildBinding (BiMeta meta) (Subst mp) = case Map.lookup meta mp of-  Just (MvBindings bds) -> Right bds-  _ -> Left meta+  Just (MvBindings bds) -> uniqueBindings bds+  _ -> Left (metaMsg meta) buildBinding (BiDelta bytes) subst = do   bts <- buildBytes bytes subst   Right [BiDelta bts] buildBinding (BiMetaLambda meta) (Subst mp) = case Map.lookup meta mp of   Just (MvFunction func) -> Right [BiLambda func]-  _ -> Left meta+  _ -> Left (metaMsg meta) buildBinding binding _ = Right [binding]  -- Build bindings that may contain meta binding (BiMeta)-buildBindings :: [Binding] -> Subst -> Either String [Binding]+buildBindings :: [Binding] -> Subst -> Built [Binding] buildBindings [] _ = Right [] buildBindings (bd : rest) subst = do   first <- buildBinding bd subst@@ -130,7 +133,7 @@ -- where X is built expression and Y is context of X -- If meta expression is built from MvExpression, is has -- context from original Program. It have default context otherwise-buildExpression :: Expression -> Subst -> Either String (Expression, Expression)+buildExpression :: Expression -> Subst -> Built (Expression, Expression) buildExpression (ExDispatch expr attr) subst = do   (dispatched, scope) <- buildExpression expr subst   attr <- buildAttribute attr subst@@ -140,38 +143,42 @@   bds <- buildBinding (BiTau battr bexpr) subst   Right (ExApplication applied (head bds), scope) buildExpression (ExFormation bds) subst = do-  bds' <- buildBindings bds subst+  bds' <- buildBindings bds subst >>= uniqueBindings   Right (ExFormation bds', defaultScope) buildExpression (ExMeta meta) (Subst mp) = case Map.lookup meta mp of-  Just (MvExpression expr scope) -> Right (expr, scope)-  _ -> Left meta+  Just (MvExpression expr scope) ->+    let res = Right (expr, scope)+     in case expr of+          ExFormation bds -> uniqueBindings bds >> res+          _ -> res+  _ -> Left (metaMsg meta) buildExpression (ExMetaTail expr meta) subst = do   let (Subst mp) = subst   (expression, scope) <- buildExpression expr subst   case Map.lookup meta mp of     Just (MvTail tails) -> Right (buildExpressionWithTails expression tails subst, scope)-    _ -> Left meta+    _ -> Left (metaMsg meta) buildExpression expr _ = Right (expr, defaultScope)  buildBytesThrows :: Bytes -> Subst -> IO Bytes buildBytesThrows bytes subst = case buildBytes bytes subst of   Right bts -> pure bts-  Left meta -> throwIO (CouldNotBuildBytes bytes meta)+  Left msg -> throwIO (CouldNotBuildBytes bytes msg)  buildBindingThrows :: Binding -> Subst -> IO [Binding] buildBindingThrows bd subst = case buildBinding bd subst of   Right bds -> pure bds-  Left meta -> throwIO (CouldNotBuildBinding bd meta)+  Left msg -> throwIO (CouldNotBuildBinding bd msg)  buildAttributeThrows :: Attribute -> Subst -> IO Attribute buildAttributeThrows attr subst = case buildAttribute attr subst of   Right attr' -> pure attr'-  Left meta -> throwIO (CouldNotBuildAttribute attr meta)+  Left msg -> throwIO (CouldNotBuildAttribute attr msg)  buildExpressionThrows :: Expression -> Subst -> IO (Expression, Expression) buildExpressionThrows expr subst = case buildExpression expr subst of   Right built -> pure built-  Left meta -> throwIO (CouldNotBuildExpression expr meta)+  Left msg -> throwIO (CouldNotBuildExpression expr msg)  -- Build a several expression from one expression and several substitutions buildExpressions :: Expression -> [Subst] -> IO [(Expression, Expression)]
src/CLI.hs view
@@ -15,17 +15,20 @@ import Control.Monad (when) import Data.Char (toLower, toUpper) import Data.List (intercalate)+import Data.Maybe (isJust, isNothing) import Data.Version (showVersion) import Dataize (DataizeContext (DataizeContext), dataize) import Functions (buildTerm) import qualified Functions+import LaTeX (explainRules, programToLaTeX) import Logger import Misc (ensuredFile) import qualified Misc+import Must (Must (..)) import Options.Applicative import Parser (parseProgramThrows) import Paths_phino (version)-import Pretty (PrintMode (SALTY, SWEET), prettyBytes, prettyProgram')+import Pretty (PrintMode (SALTY, SWEET), prettyBytes, prettyProgram', Encoding (UNICODE)) import Rewriter (RewriteContext (RewriteContext), rewrite') import System.Exit (ExitCode (..), exitFailure) import System.IO (getContents')@@ -48,22 +51,33 @@ data Command   = CmdRewrite OptsRewrite   | CmdDataize OptsDataize+  | CmdExplain OptsExplain -data IOFormat = XMIR | PHI+data IOFormat = XMIR | PHI | LATEX   deriving (Eq)  instance Show IOFormat where   show XMIR = "xmir"   show PHI = "phi"+  show LATEX = "latex"  data OptsDataize = OptsDataize   { logLevel :: LogLevel,     inputFormat :: IOFormat,     maxDepth :: Integer,     maxCycles :: Integer,+    depthSensitive :: Bool,     inputFile :: Maybe FilePath   } +data OptsExplain = OptsExplain+  { logLevel :: LogLevel,+    rules :: [FilePath],+    normalize :: Bool,+    shuffle :: Bool,+    targetFile :: Maybe FilePath+  }+ data OptsRewrite = OptsRewrite   { logLevel :: LogLevel,     rules :: [FilePath],@@ -71,22 +85,24 @@     outputFormat :: IOFormat,     printMode :: PrintMode,     normalize :: Bool,-    nothing :: Bool,     shuffle :: Bool,     omitListing :: Bool,     omitComments :: Bool,-    must :: Integer,+    depthSensitive :: Bool,+    must :: Must,     maxDepth :: Integer,     maxCycles :: Integer,+    inPlace :: Bool,     targetFile :: Maybe FilePath,     inputFile :: Maybe FilePath   }  parseIOFormat :: String -> ReadM IOFormat-parseIOFormat type' = eitherReader $ \format -> case map toLower format of-  "xmir" -> Right XMIR-  "phi" -> Right PHI-  _ -> Left (printf "invalid %s format: expected '%s' or '%s'" type' (show PHI) (show XMIR))+parseIOFormat type' = eitherReader $ \format -> case (map toLower format, type') of+  ("xmir", _) -> Right XMIR+  ("phi", _) -> Right PHI+  ("latex", "output") -> Right LATEX+  _ -> Left (printf "The value '%s' can't be used for '--%s' option, use --help to check possible values" format type')  argInputFile :: Parser (Maybe FilePath) argInputFile = optional (argument str (metavar "FILE" <> help "Path to input file"))@@ -98,8 +114,11 @@ optMaxCycles = option auto (long "max-cycles" <> metavar "CYCLES" <> help "Maximum number of rewriting cycles across all rules" <> value 25 <> showDefault)  optInputFormat :: Parser IOFormat-optInputFormat = option (parseIOFormat "input") (long "input" <> metavar "FORMAT" <> help "Program input format (phi, xmir)" <> value PHI <> showDefault)+optInputFormat = option (parseIOFormat "input") (long "input" <> metavar "FORMAT" <> help "Program input format (phi, xmir, latex)" <> value PHI <> showDefault) +optDepthSensitive :: Parser Bool+optDepthSensitive = switch (long "depth-sensitive" <> help "Fail if rewriting is not finished after reaching max attempts (see --max-cycles or --max-depth)")+ optLogLevel :: Parser LogLevel optLogLevel =   option@@ -122,6 +141,29 @@       "NONE" -> Right NONE       _ -> Left $ "unknown log-level: " <> lvl +optRule :: Parser [FilePath]+optRule = many (strOption (long "rule" <> metavar "FILE" <> help "Path to custom rule"))++optNormalize :: Parser Bool+optNormalize = switch (long "normalize" <> help "Use built-in normalization rules")++optTarget :: Parser (Maybe FilePath)+optTarget = optional (strOption (long "target" <> short 't' <> metavar "FILE" <> help "File to save output to"))++optShuffle :: Parser Bool+optShuffle = switch (long "shuffle" <> help "Shuffle rules before applying")++explainParser :: Parser Command+explainParser =+  CmdExplain+    <$> ( OptsExplain+            <$> optLogLevel+            <*> optRule+            <*> optNormalize+            <*> optShuffle+            <*> optTarget+        )+ dataizeParser :: Parser Command dataizeParser =   CmdDataize@@ -130,6 +172,7 @@             <*> optInputFormat             <*> optMaxDepth             <*> optMaxCycles+            <*> optDepthSensitive             <*> argInputFile         ) @@ -138,29 +181,36 @@   CmdRewrite     <$> ( OptsRewrite             <$> optLogLevel-            <*> many (strOption (long "rule" <> metavar "FILE" <> help "Path to custom rule"))+            <*> optRule             <*> optInputFormat             <*> option (parseIOFormat "output") (long "output" <> metavar "FORMAT" <> help "Program output format (phi, xmir)" <> value PHI <> showDefault)             <*> flag SALTY SWEET (long "sweet" <> help "Print 𝜑-program using syntax sugar")-            <*> switch (long "normalize" <> help "Use built-in normalization rules")-            <*> switch (long "nothing" <> help "Just desugar provided 𝜑-program")-            <*> switch (long "shuffle" <> help "Shuffle rules before applying")+            <*> optNormalize+            <*> optShuffle             <*> switch (long "omit-listing" <> help "Omit full program listing in XMIR output")             <*> switch (long "omit-comments" <> help "Omit comments in XMIR output")-            <*> ( flag' 1 (long "must" <> help "Enable must-rewrite with default value 1")-                    <|> option auto (long "must" <> metavar "N" <> help "Must-rewrite, stops execution if not exactly N rules applied (default 1 when specified without value, if 0 - flag is disabled)" <> value 0)-                )+            <*> optDepthSensitive+            <*> option+              auto+              ( long "must"+                  <> metavar "RANGE"+                  <> help "Must-rewrite range (e.g., '3', '..5', '3..', '3..5'). Stops execution if number of rules applied is not in range. Use 0 to disable."+                  <> value MtDisabled+                  <> showDefaultWith show+              )             <*> optMaxDepth             <*> optMaxCycles-            <*> optional (strOption (long "target" <> short 't' <> metavar "FILE" <> help "File to save output to"))+            <*> switch (long "in-place" <> help "Edit file in-place instead of printing to output")+            <*> optTarget             <*> argInputFile         )  commandParser :: Parser Command commandParser =   hsubparser-    ( command "rewrite" (info (rewriteParser <**> helper) (progDesc "Rewrite the program"))-        <> command "dataize" (info (dataizeParser <**> helper) (progDesc "Dataize the program"))+    ( command "rewrite" (info rewriteParser (progDesc "Rewrite the program"))+        <> command "dataize" (info dataizeParser (progDesc "Dataize the program"))+        <> command "explain" (info explainParser (progDesc "Explain rules in LaTeX format"))     )  parserInfo :: ParserInfo Command@@ -181,6 +231,7 @@   let level = case cmd of         CmdRewrite OptsRewrite {logLevel} -> logLevel         CmdDataize OptsDataize {logLevel} -> logLevel+        CmdExplain OptsExplain {logLevel} -> logLevel    in setLogLevel level  runCLI :: [String] -> IO ()@@ -188,65 +239,72 @@   cmd <- handleParseResult (execParserPure defaultPrefs parserInfo args)   setLogLevel' cmd   case cmd of-    CmdRewrite OptsRewrite {..} -> do-      validateMaxDepth maxDepth-      validateMaxCycles maxCycles-      validateMust must-      logDebug (printf "Amount of rewriting cycles: %d" maxDepth)+    CmdRewrite opts@OptsRewrite {..} -> do+      validateOpts+      logDebug (printf "Amount of rewriting cycles across all the rules: %d, per rule: %d" maxCycles maxDepth)       input <- readInput inputFile-      rules' <- getRules+      rules' <- getRules normalize shuffle rules       program <- parseProgram input inputFormat-      rewritten <- rewrite' program rules' (RewriteContext program maxDepth maxCycles buildTerm must)+      rewritten <- rewrite' program rules' (RewriteContext program maxDepth maxCycles depthSensitive buildTerm must)       logDebug (printf "Printing rewritten 𝜑-program as %s" (show outputFormat))-      prog <- printProgram rewritten outputFormat printMode-      output prog+      prog <- printProgram rewritten outputFormat printMode input+      output targetFile prog       where-        getRules :: IO [Y.Rule]-        getRules = do-          ordered <--            if nothing-              then do-                logDebug "The --nothing option is provided, no rules are used"-                pure []-              else-                if normalize-                  then do-                    let rules' = normalizationRules-                    logDebug (printf "The --normalize option is provided, %d built-it normalization rules are used" (length rules'))-                    pure rules'-                  else-                    if null rules-                      then throwIO (InvalidRewriteArguments "no --rule, no --normalize, no --nothing are provided")-                      else do-                        logDebug (printf "Using rules from files: [%s]" (intercalate ", " rules))-                        yamls <- mapM ensuredFile rules-                        mapM Y.yamlRule yamls-          if shuffle-            then do-              logDebug "The --shuffle option is provided, rules are used in random order"-              Misc.shuffle ordered-            else pure ordered-        printProgram :: Program -> IOFormat -> PrintMode -> IO String-        printProgram prog PHI mode = pure (prettyProgram' prog mode)-        printProgram prog XMIR mode = do-          xmir <- programToXMIR prog (XmirContext omitListing omitComments printMode)+        validateOpts :: IO ()+        validateOpts = do+          when+            (printMode == SWEET && outputFormat == XMIR)+            (throwIO (InvalidRewriteArguments "The --sweet and --output=xmir can't stay together"))+          when+            (inPlace && isNothing inputFile)+            (throwIO (InvalidRewriteArguments "--in-place requires an input file"))+          when+            (inPlace && isJust targetFile)+            (throwIO (InvalidRewriteArguments "--in-place and --target cannot be used together"))+          validateMaxDepth maxDepth+          validateMaxCycles maxCycles+          validateMust must+        printProgram :: Program -> IOFormat -> PrintMode -> String -> IO String+        printProgram prog PHI mode _ = pure (prettyProgram' prog mode UNICODE)+        printProgram prog XMIR _ listing = do+          xmir <- programToXMIR prog (XmirContext omitListing omitComments listing)           pure (printXMIR xmir)-        output :: String -> IO ()-        output prog = case targetFile of-          Nothing -> do-            logDebug "The option '--target' is not specified, printing to console..."-            putStrLn prog-          Just file -> do+        printProgram prog LATEX mode _ = pure (programToLaTeX prog mode)+        output :: Maybe FilePath -> String -> IO ()+        output target prog = case (inPlace, target, inputFile) of+          (True, _, Just file) -> do+            logDebug (printf "The option '--in-place' is specified, writing back to '%s'..." file)+            writeFile file prog+            logInfo (printf "The file '%s' was modified in-place" file)+          (False, Just file, _) -> do             logDebug (printf "The option '--target' is specified, printing to '%s'..." file)             writeFile file prog-            logInfo (printf "The result program was saved in '%s'" file)-    CmdDataize OptsDataize {..} -> do-      validateMaxDepth maxDepth-      validateMaxCycles maxCycles+            logInfo (printf "The command result was saved in '%s'" file)+          (False, Nothing, _) -> do+            logDebug "The option '--target' is not specified, printing to console..."+            putStrLn prog+    CmdDataize opts@OptsDataize {..} -> do+      validateOpts       input <- readInput inputFile       prog <- parseProgram input inputFormat-      dataized <- dataize prog (DataizeContext prog maxDepth maxCycles buildTerm)+      dataized <- dataize prog (DataizeContext prog maxDepth maxCycles depthSensitive buildTerm)       maybe (throwIO CouldNotDataize) (putStrLn . prettyBytes) dataized+      where+        validateOpts :: IO ()+        validateOpts = do+          validateMaxDepth maxDepth+          validateMaxCycles maxCycles+    CmdExplain opts@OptsExplain {..} -> do+      validateOpts+      rules' <- getRules normalize shuffle rules+      let latex = explainRules rules'+      output targetFile (explainRules rules')+      where+        validateOpts :: IO ()+        validateOpts =+          when+            (null rules && not normalize)+            (throwIO (InvalidRewriteArguments "Either --rule or --normalize must be specified"))   where     validateIntArgument :: Integer -> (Integer -> Bool) -> String -> IO ()     validateIntArgument num cmp msg =@@ -257,8 +315,20 @@     validateMaxDepth depth = validateIntArgument depth (<= 0) "--max-depth must be positive"     validateMaxCycles :: Integer -> IO ()     validateMaxCycles cycles = validateIntArgument cycles (<= 0) "--max-cycles must be positive"-    validateMust :: Integer -> IO ()-    validateMust must = validateIntArgument must (< 0) "--must must be positive"+    validateMust :: Must -> IO ()+    validateMust MtDisabled = pure ()+    validateMust (MtExact n) = validateIntArgument n (<= 0) "--must exact value must be positive"+    validateMust (MtRange minVal maxVal) = do+      maybe (pure ()) (\n -> validateIntArgument n (< 0) "--must minimum must be non-negative") minVal+      maybe (pure ()) (\n -> validateIntArgument n (< 0) "--must maximum must be non-negative") maxVal+      case (minVal, maxVal) of+        (Just min, Just max)+          | min > max ->+              throwIO+                ( InvalidRewriteArguments+                    (printf "--must range invalid: minimum (%d) is greater than maximum (%d)" min max)+                )+        _ -> pure ()     readInput :: Maybe FilePath -> IO String     readInput inputFile' = case inputFile' of       Just pth -> do@@ -272,3 +342,34 @@     parseProgram xmir XMIR = do       doc <- parseXMIRThrows xmir       xmirToPhi doc+    getRules :: Bool -> Bool -> [FilePath] -> IO [Y.Rule]+    getRules normalize shuffle rules = do+      ordered <-+        if normalize+          then do+            let rules' = normalizationRules+            logDebug (printf "The --normalize option is provided, %d built-it normalization rules are used" (length rules'))+            pure rules'+          else+            if null rules+              then do+                logDebug "No --rule and no --normalize options are provided, no rules are used"+                pure []+              else do+                logDebug (printf "Using rules from files: [%s]" (intercalate ", " rules))+                yamls <- mapM ensuredFile rules+                mapM Y.yamlRule yamls+      if shuffle+        then do+          logDebug "The --shuffle option is provided, rules are used in random order"+          Misc.shuffle ordered+        else pure ordered+    output :: Maybe FilePath -> String -> IO ()+    output target content = case target of+      Nothing -> do+        logDebug "The option '--target' is not specified, printing to console..."+        putStrLn content+      Just file -> do+        logDebug (printf "The option '--target' is specified, printing to '%s'..." file)+        writeFile file content+        logInfo (printf "The command result was saved in '%s'" file)
src/Dataize.hs view
@@ -11,6 +11,7 @@ import Control.Exception (throwIO) import Data.List (partition) import Misc+import Must (Must (..)) import Rewriter (RewriteContext (RewriteContext), rewrite') import Rule (RuleContext (RuleContext), isNF) import Term (BuildTermFunc)@@ -22,11 +23,12 @@   { _program :: Program,     _maxDepth :: Integer,     _maxCycles :: Integer,+    _depthSensitive :: Bool,     _buildTerm :: BuildTermFunc   }  switchContext :: DataizeContext -> RewriteContext-switchContext DataizeContext {..} = RewriteContext _program _maxDepth _maxCycles _buildTerm 0+switchContext DataizeContext {..} = RewriteContext _program _maxDepth _maxCycles _depthSensitive _buildTerm MtDisabled  maybeBinding :: (Binding -> Bool) -> [Binding] -> (Maybe Binding, [Binding]) maybeBinding _ [] = (Nothing, [])
src/Functions.hs view
@@ -14,6 +14,7 @@ import Data.Functor import Data.Set (Set) import qualified Data.Set+import qualified Data.Set as Set import GHC.IO (unsafePerformIO) import Matcher import Misc@@ -40,6 +41,7 @@ buildTerm "string" = _string buildTerm "number" = _number buildTerm "sum" = _sum+buildTerm "join" = _join buildTerm func = _unsupported func  argToBytes :: Y.ExtraArgument -> Subst -> Program -> IO Bytes@@ -85,17 +87,8 @@       Y.ArgBinding bd -> do         bds <- buildBindingThrows bd subst         rest' <- argsToAttrs rest-        pure (attrsFromBindings bds ++ rest')+        pure (map show (attributesFromBindings bds) ++ rest')       Y.ArgBytes _ -> throwIO (userError "Bytes can't be argument of random-tau() function")-    attrsFromBindings :: [Binding] -> [String]-    attrsFromBindings [] = []-    attrsFromBindings (bd : bds) =-      let attr = case bd of-            BiTau attr _ -> attr-            BiDelta _ -> AtDelta-            BiLambda _ -> AtLambda-            BiVoid attr -> attr-       in prettyAttribute attr : attrsFromBindings bds     randomTau :: [String] -> IO String     randomTau attrs = do       tau <- randomString "a🌵%d"@@ -216,6 +209,38 @@ _sum args subst prog = do   nums <- traverse (\arg -> argToNumber arg subst prog) args   pure (TeExpression (DataNumber (numToBts (sum nums))))++_join :: BuildTermMethod+_join [] _ _ = pure (TeBindings [])+_join args subst _ = do+  bds <- buildBindings args+  pure (TeBindings (join' bds Set.empty))+  where+    buildBindings :: [Y.ExtraArgument] -> IO [Binding]+    buildBindings [] = pure []+    buildBindings (Y.ArgBinding bd : args') = do+      bds <- buildBindingThrows bd subst+      next <- buildBindings args'+      pure (bds ++ next)+    buildBindings _ = throwIO (userError "Function 'join' can work with bindings only")+    join' :: [Binding] -> Set.Set Attribute -> [Binding]+    join' [] _ = []+    join' (bd : bds) attrs =+      let [attr] = attributesFromBindings [bd]+       in if Set.member attr attrs+            then+              if attr == AtRho || attr == AtDelta || attr == AtLambda+                then join' bds attrs+                else+                  let new = case bd of+                        BiTau attr expr -> BiTau (updated attr attrs) expr+                        BiVoid attr -> BiVoid (updated attr attrs)+                   in new : join' bds attrs+            else bd : join' bds (Set.insert attr attrs)+    updated :: Attribute -> Set.Set Attribute -> Attribute+    updated attr attrs =+      let (TeAttribute attr') = unsafePerformIO (_randomTau (map Y.ArgAttribute (Set.toList attrs)) subst (Program ExGlobal))+       in attr'  _unsupported :: BuildTermFunc _unsupported func _ _ _ = throwIO (userError (printf "Function %s() is not supported or does not exist" func))
+ src/LaTeX.hs view
@@ -0,0 +1,44 @@+-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com+-- SPDX-License-Identifier: MIT++module LaTeX (explainRules, programToLaTeX) where++import Data.List (intercalate)+import Data.Maybe (fromMaybe)+import qualified Yaml as Y+import Ast (Program)+import Pretty (PrintMode, prettyProgram', Encoding (ASCII))++programToLaTeX :: Program -> PrintMode -> String+programToLaTeX prog mode = unlines+  [+    "\\documentclass{article}",+    "\\usepackage{eolang}",+    "\\begin{document}",+    "\\begin{phiquation}",+    prettyProgram' prog mode ASCII,+    "\\end{phiquation}",+    "\\end{document}"+  ]++-- @todo #114:30min Implement LaTeX conversion for rules.+--  Convert Rule data structure to LaTeX inference rule format.+--  Each rule should be formatted as a LaTeX inference rule with+--  pattern, result, and optional conditions.+--  Tests must be added for LaTeX conversion logic.+explainRule :: Y.Rule -> String+explainRule rule = "\\rule{" ++ fromMaybe "unnamed" (Y.name rule) ++ "}"++-- @todo #114:30min Create LaTeX document wrapper.+--  Generate proper LaTeX document with tabular format for rules.+--  Each rule should be   in its own tabular environment.+--  Include tests for document structure generation.+explainRules :: [Y.Rule] -> String+explainRules rules' =+  unlines+    [ "\\documentclass{article}",+      "\\usepackage{amsmath}",+      "\\begin{document}"+    ]+    ++ unlines (map explainRule rules')+    ++ "\\end{document}"
src/Misc.hs view
@@ -20,6 +20,10 @@     shuffle,     toDouble,     btsToUnescapedStr,+    attributesFromBindings,+    uniqueBindings,+    uniqueBindings',+    validateYamlObject,     pattern DataObject,     pattern DataString,     pattern DataNumber,@@ -38,6 +42,7 @@ import qualified Data.ByteString.Lazy.UTF8 as U import Data.Char (chr, isPrint, ord) import Data.List (intercalate)+import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Vector as V@@ -48,6 +53,9 @@ import System.FilePath ((</>)) import System.Random.Stateful import Text.Printf (printf)+import Data.Aeson (Object)+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.Aeson.Key as Key  data FsException   = FileDoesNotExist {file :: FilePath}@@ -99,6 +107,52 @@             )         ) +-- Extract attributes from bindings+attributesFromBindings :: [Binding] -> [Attribute]+attributesFromBindings [] = []+attributesFromBindings (bd : bds) =+  let attr = case bd of+        BiTau attr _ -> Just attr+        BiDelta _ -> Just AtDelta+        BiLambda _ -> Just AtLambda+        BiVoid attr -> Just attr+        BiMeta _ -> Nothing+        BiMetaLambda _ -> Just AtLambda+   in case attr of+        Just attr' -> attr' : attributesFromBindings bds+        _ -> attributesFromBindings bds++uniqueBindings' :: [Binding] -> IO [Binding]+uniqueBindings' bds = case uniqueBindings bds of+  Left msg -> throwIO (userError msg)+  Right _ -> pure bds++-- Check if given binding list consists of unique attributes+uniqueBindings :: [Binding] -> Either String [Binding]+uniqueBindings bds = case maybeDuplicatedAttribute bds Set.empty of+  Just attr ->+    Left+      ( printf+          "Duplicated attribute '%s' found in %s"+          (show attr)+          (intercalate ", " (map show (attributesFromBindings bds)))+      )+  _ -> Right bds+  where+    maybeDuplicatedAttribute :: [Binding] -> Set.Set Attribute -> Maybe Attribute+    maybeDuplicatedAttribute [] = const Nothing+    maybeDuplicatedAttribute ((BiTau attr _) : rest) = checkAttr attr rest+    maybeDuplicatedAttribute (BiVoid attr : rest) = checkAttr attr rest+    maybeDuplicatedAttribute (BiLambda _ : rest) = checkAttr AtLambda rest+    maybeDuplicatedAttribute (BiMetaLambda _ : rest) = checkAttr AtLambda rest+    maybeDuplicatedAttribute (BiDelta _ : rest) = checkAttr AtDelta rest+    maybeDuplicatedAttribute (BiMeta _ : rest) = maybeDuplicatedAttribute rest++    checkAttr :: Attribute -> [Binding] -> Set.Set Attribute -> Maybe Attribute+    checkAttr attr rest acc+      | attr `Set.member` acc = Just attr+      | otherwise = maybeDuplicatedAttribute rest (Set.insert attr acc)+ -- Add void rho binding to the end of the list of any rho binding is not present withVoidRho :: [Binding] -> [Binding] withVoidRho bds = withVoidRho' bds False@@ -295,3 +349,14 @@     j <- uniformRM (0, i) gen     M.swap v i j   V.toList <$> V.freeze v++validateYamlObject :: (Applicative a, MonadFail a) => Object -> [String] -> a ()+validateYamlObject v keys = do+  let present = filter (`KeyMap.member` v) (map Key.fromString keys)+      current = KeyMap.keys v+  when+    (length current > 1)+    (fail ("Exactly one condition type is expected, when multiple condition types specified: " ++ show current))+  when+    (null present)+    (fail (printf "Unknown condition type '%s', expected one of: %s" (show current) (show keys)))
+ src/Must.hs view
@@ -0,0 +1,65 @@+-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com+-- SPDX-License-Identifier: MIT++module Must (Must (..), inRange, exceedsUpperBound) where++import Data.List (isInfixOf)+import Text.Read (readMaybe)++data Must+  = MtDisabled+  | MtExact Integer+  | MtRange (Maybe Integer) (Maybe Integer)+  deriving (Eq)++instance Show Must where+  show MtDisabled = "0"+  show (MtExact n) = show n+  show (MtRange Nothing Nothing) = ".."+  show (MtRange Nothing (Just max)) = ".." ++ show max+  show (MtRange (Just min) Nothing) = show min ++ ".."+  show (MtRange (Just min) (Just max)) = show min ++ ".." ++ show max++instance Read Must where+  readsPrec _ "0" = [(MtDisabled, "")]+  readsPrec _ s+    | ".." `isInfixOf` s = parseRange s+    | otherwise = parseExact s+    where+      parseRange :: String -> [(Must, String)]+      parseRange str = case break (== '.') str of+        (minStr, '.' : '.' : maxStr) ->+          let minPart = if null minStr then Nothing else readMaybe minStr+              maxPart = if null maxStr then Nothing else readMaybe maxStr+           in case (minPart, maxPart, null minStr, null maxStr) of+                (Nothing, Nothing, False, False) -> [] -- Invalid range: non-numeric values+                (Nothing, Nothing, True, True) -> [] -- Invalid range: empty range '..'+                (Nothing, Just max, True, False) ->+                  [(MtRange Nothing (Just max), "") | max >= 0]+                (Just min, Nothing, False, True) ->+                  [(MtRange (Just min) Nothing, "") | min >= 0]+                (Just min, Just max, False, False) ->+                  [(MtRange (Just min) (Just max), "") | min >= 0 && max >= 0 && min <= max]+                _ -> [] -- Invalid range format+        _ -> [] -- Invalid range: expected format like '3..5', '3..', or '..5'+      parseExact :: String -> [(Must, String)]+      parseExact str = case readMaybe str of+        Just n | n >= 0 -> [(if n == 0 then MtDisabled else MtExact n, "")]+        Just _ -> [] -- Invalid value: must be non-negative+        Nothing -> [] -- Invalid value: expected integer++inRange :: Must -> Integer -> Bool+inRange MtDisabled _ = True+inRange (MtExact expected) actual = actual == expected+inRange (MtRange minVal maxVal) actual =+  checkMin && checkMax+  where+    checkMin = maybe True (<= actual) minVal+    checkMax = maybe True (>= actual) maxVal++-- | Check if a value exceeds the upper bound of the range+exceedsUpperBound :: Must -> Integer -> Bool+exceedsUpperBound MtDisabled _ = False+exceedsUpperBound (MtExact n) current = current > n+exceedsUpperBound (MtRange _ (Just max)) current = current > max+exceedsUpperBound (MtRange _ Nothing) _ = False
src/Parser.hs view
@@ -22,7 +22,9 @@ import Ast import Control.Exception (Exception, throwIO) import Control.Monad (guard)+import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Char (isAsciiLower, isDigit, isLower)+import Data.List (intercalate) import Data.Scientific (toRealFloat) import Data.Sequence (mapWithIndex) import Data.Text.Internal.Fusion.Size (lowerBound)@@ -82,7 +84,7 @@     'f' -> return '\f'     'u' -> unicodeEscape     'x' -> hexEscape-    _ -> fail $ "Unknown escape: \\" ++ [c]+    _ -> fail ("Unknown escape: \\" ++ [c])   where     unicodeEscape :: Parser Char     unicodeEscape = do@@ -115,7 +117,7 @@       digits <- count 2 hexDigitChar       case readHex digits of         [(n, "")] -> return (chr n)-        _ -> fail $ "Invalid hex escape: \\x" ++ digits+        _ -> fail ("Invalid hex escape: \\x" ++ digits)  function :: Parser String function = lexeme $ do@@ -230,8 +232,9 @@         voids <- map BiVoid <$> void' `sepBy` symbol ","         _ <- symbol ")"         _ <- arrow-        ExFormation bs <- formation-        return (BiTau attr' (ExFormation (withVoidRho (voids ++ bs))))+        bs <- formationBindings+        bds <- validatedBindings (voids ++ bs)+        return (BiTau attr' (ExFormation (withVoidRho bds)))     ]  metaBinding :: Parser Binding@@ -312,13 +315,18 @@     ]     <?> "full attribute" +validatedBindings :: [Binding] -> Parser [Binding]+validatedBindings bds = case uniqueBindings bds of+  Left msg -> fail msg+  Right bds' -> return bds'+ -- formation-formation :: Parser Expression-formation = do+formationBindings :: Parser [Binding]+formationBindings = do   _ <- choice [symbol "[[", symbol "⟦"]   bs <- binding `sepBy` symbol ","   _ <- choice [symbol "]]", symbol "⟧"]-  return (ExFormation bs)+  return bs  -- head part of expression -- 1. formation@@ -331,7 +339,7 @@ exHead =   choice     [ do-        ExFormation bs <- formation+        bs <- formationBindings >>= validatedBindings         return (ExFormation (withVoidRho bs)),       do         _ <- choice [symbol "$", symbol "ξ"]@@ -351,7 +359,7 @@         str <- manyTill (choice [escapedChar, noneOf ['\\', '"']]) (char '"')         return (DataString (strToBts str)),       try (ExMeta <$> meta' 'e' "𝑒"),-      ExDispatch ExThis <$> fullAttribute+      ExDispatch ExThis <$> attribute     ]     <?> "expression head" @@ -370,7 +378,7 @@           choice             [ do                 _ <- symbol "."-                ExDispatch expr <$> fullAttribute,+                ExDispatch expr <$> attribute,               do                 guard                   ( case expr of
src/Pretty.hs view
@@ -16,6 +16,7 @@     prettyBytes,     prettyExtraArg,     PrintMode (SWEET, SALTY),+    Encoding (ASCII, UNICODE),   ) where @@ -35,27 +36,43 @@   show SWEET = "sweet"   show SALTY = "salty" -newtype Formatted a = Formatted {unFormatted :: (PrintMode, a)}+data Encoding = ASCII | UNICODE +newtype Formatted a = Formatted {unFormatted :: (PrintMode, Encoding, a)}++formatted :: a -> Formatted a+formatted x = Formatted (SALTY, UNICODE, x)+ prettyMeta :: String -> Doc ann prettyMeta meta = pretty "!" <> pretty meta -prettyArrow :: Doc ann-prettyArrow = pretty "↦"+prettyArrow :: Encoding -> Doc ann+prettyArrow UNICODE = pretty "↦"+prettyArrow ASCII = pretty "->" -prettyLsb :: Doc ann-prettyLsb = pretty "⟦"+prettyLsb :: Encoding -> Doc ann+prettyLsb UNICODE = pretty "⟦"+prettyLsb ASCII = pretty "[[" -prettyRsb :: Doc ann-prettyRsb = pretty "⟧"+prettyRsb :: Encoding -> Doc ann+prettyRsb UNICODE = pretty "⟧"+prettyRsb ASCII = pretty "]]"  prettyDashedArrow :: Doc ann prettyDashedArrow = pretty "⤍" +prettyLambda :: Encoding -> Doc ann+prettyLambda UNICODE = pretty "λ" <+> prettyDashedArrow+prettyLambda ASCII = pretty "L>"++prettyDelta :: Encoding -> Doc ann+prettyDelta UNICODE = pretty "Δ" <+> prettyDashedArrow+prettyDelta ASCII = pretty "D>"+ instance Pretty ExtraArgument where-  pretty (ArgExpression expr) = pretty (Formatted (SWEET, expr))-  pretty (ArgBinding bd) = pretty (Formatted (SWEET, bd))-  pretty (ArgAttribute attr) = pretty attr+  pretty (ArgExpression expr) = pretty (Formatted (SWEET, UNICODE, expr))+  pretty (ArgBinding bd) = pretty (Formatted (SWEET, UNICODE, bd))+  pretty (ArgAttribute attr) = pretty (formatted attr)   pretty (ArgBytes bytes) = pretty bytes  instance Pretty Bytes where@@ -64,24 +81,22 @@   pretty (BtMany bts) = pretty (intercalate "-" bts)   pretty (BtMeta meta) = prettyMeta meta -instance Pretty Attribute where-  pretty (AtLabel name) = pretty name-  pretty (AtAlpha index) = pretty "α" <> pretty index-  pretty AtRho = pretty "ρ"-  pretty AtPhi = pretty "φ"-  pretty AtDelta = pretty "Δ"-  pretty AtLambda = pretty "λ"-  pretty (AtMeta meta) = prettyMeta meta+instance Pretty (Formatted Attribute) where+  pretty (Formatted (_, _, AtMeta meta)) = prettyMeta meta+  pretty (Formatted (_, ASCII, AtAlpha idx)) = pretty "~" <> pretty idx+  pretty (Formatted (_, ASCII, AtPhi)) = pretty "@"+  pretty (Formatted (_, ASCII, AtRho)) = pretty "^"+  pretty (Formatted (_, _, attr)) = pretty (show attr)  instance Pretty (Formatted Binding) where-  pretty (Formatted (SWEET, BiTau attr (ExFormation bindings))) =+  pretty (Formatted (SWEET, encoding, BiTau attr (ExFormation bindings))) =     let voids' = voids bindings      in if null voids'-          then pretty attr <+> prettyArrow <+> pretty (Formatted (SWEET, ExFormation bindings))+          then pretty (Formatted (SWEET, encoding, attr)) <+> prettyArrow encoding <+> pretty (Formatted (SWEET, encoding, ExFormation bindings))           else             if length voids' == length bindings && last voids' == AtRho-              then inlineVoids (init voids') <+> prettyLsb <> prettyRsb-              else inlineVoids voids' <+> pretty (Formatted (SWEET, ExFormation (drop (length voids') bindings)))+              then inlineVoids (init voids') <+> prettyLsb encoding <> prettyRsb encoding+              else inlineVoids voids' <+> pretty (Formatted (SWEET, encoding, ExFormation (drop (length voids') bindings)))     where       voids :: [Binding] -> [Attribute]       voids [] = []@@ -89,19 +104,27 @@         BiVoid attr -> attr : voids bds         _ -> []       inlineVoids :: [Attribute] -> Doc ann-      inlineVoids [] = pretty attr <+> prettyArrow-      inlineVoids voids' = pretty attr <> lparen <> hsep (punctuate comma (map pretty voids')) <> rparen <+> prettyArrow-  pretty (Formatted (mode, BiTau attr expr)) = pretty attr <+> prettyArrow <+> pretty (Formatted (mode, expr))-  pretty (Formatted (_, BiMeta meta)) = prettyMeta meta-  pretty (Formatted (_, BiDelta bytes)) = pretty "Δ" <+> prettyDashedArrow <+> pretty bytes-  pretty (Formatted (_, BiMetaLambda meta)) = pretty "λ" <+> prettyDashedArrow <+> prettyMeta meta-  pretty (Formatted (_, BiVoid attr)) = pretty attr <+> prettyArrow <+> pretty "∅"-  pretty (Formatted (_, BiLambda func)) = pretty "λ" <+> prettyDashedArrow <+> pretty func+      inlineVoids [] = pretty (Formatted (SWEET, encoding, attr)) <+> prettyArrow encoding+      inlineVoids voids' =+        pretty (Formatted (SWEET, encoding, attr))+          <> lparen+          <> hsep (punctuate comma (map (\attr -> pretty (Formatted (SWEET, encoding, attr))) voids'))+          <> rparen+          <+> prettyArrow encoding+  pretty (Formatted (mode, encoding, BiTau attr expr)) = pretty (Formatted (mode, encoding, attr)) <+> prettyArrow encoding <+> pretty (Formatted (mode, encoding, expr))+  pretty (Formatted (_, _, BiMeta meta)) = prettyMeta meta+  pretty (Formatted (_, encoding, BiDelta bytes)) = prettyDelta encoding <+> pretty bytes+  pretty (Formatted (_, encoding, BiLambda func)) = prettyLambda encoding <+> pretty func+  pretty (Formatted (_, encoding, BiMetaLambda meta)) = prettyLambda encoding <+> prettyMeta meta+  pretty (Formatted (mode, UNICODE, BiVoid attr)) = pretty (Formatted (mode, UNICODE, attr)) <+> prettyArrow UNICODE <+> pretty "∅"+  pretty (Formatted (mode, ASCII, BiVoid attr)) = pretty (Formatted (mode, ASCII, attr)) <+> prettyArrow ASCII <+> pretty "?" --- >>> render (Formatted (SWEET, [BiVoid AtRho]))+-- >>> render (Formatted (SWEET, UNICODE, [BiVoid AtRho])) -- ""+-- >>> render (Formatted (SWEET, UNICODE, [BiTau (AtLabel "x") ExGlobal, BiVoid AtPhi]))+-- "x \8614 \934,\n\966 \8614 \8709" instance {-# OVERLAPPING #-} Pretty (Formatted [Binding]) where-  pretty (Formatted (SWEET, bds)) = vsep (punctuate comma (excludeVoidRho (\bd -> pretty (Formatted (SWEET, bd))) [] bds))+  pretty (Formatted (SWEET, encoding, bds)) = vsep (punctuate comma (excludeVoidRho (\bd -> pretty (Formatted (SWEET, encoding, bd))) [] bds))     where       excludeVoidRho :: (Binding -> Doc ann) -> [Doc ann] -> [Binding] -> [Doc ann]       excludeVoidRho func acc [bd] = case bd of@@ -109,7 +132,7 @@         _ -> reverse (func bd : acc)       excludeVoidRho func acc (x : xs) = excludeVoidRho func (func x : acc) xs       excludeVoidRho func acc [] = reverse acc-  pretty (Formatted (SALTY, bds)) = vsep (punctuate comma (map (\bd -> pretty (Formatted (SALTY, bd))) bds))+  pretty (Formatted (SALTY, encoding, bds)) = vsep (punctuate comma (map (\bd -> pretty (Formatted (SALTY, encoding, bd))) bds))  complexApplication :: Expression -> (Expression, [Binding], [Expression]) complexApplication (ExApplication (ExApplication expr tau) tau') =@@ -129,51 +152,57 @@ -- >>> render (Formatted (SWEET, ExFormation [BiVoid AtRho])) -- "\10214\10215" instance Pretty (Formatted Expression) where-  pretty (Formatted (SWEET, ExDispatch (ExDispatch ExGlobal (AtLabel "org")) (AtLabel "eolang"))) = pretty "Φ̇"-  pretty (Formatted (SWEET, DataString bytes)) = pretty "\"" <> pretty (btsToStr bytes) <> pretty "\""-  pretty (Formatted (SWEET, DataNumber bytes)) = either pretty pretty (btsToNum bytes)-  pretty (Formatted (SWEET, DataObject other bytes)) = pretty (Formatted (SALTY, DataObject other bytes))-  pretty (Formatted (SWEET, ExFormation [BiVoid AtRho])) = pretty "⟦⟧"-  pretty (Formatted (mode, ExFormation [binding])) = case binding of-    BiTau _ _ -> vsep [pretty "⟦", indent 2 (pretty (Formatted (mode, binding))), pretty "⟧"]-    _ -> pretty "⟦" <+> pretty (Formatted (mode, binding)) <+> pretty "⟧"-  pretty (Formatted (_, ExFormation [])) = pretty "⟦⟧"-  pretty (Formatted (mode, ExFormation bindings)) = vsep [pretty "⟦", indent 2 (pretty (Formatted (mode, bindings))), pretty "⟧"]-  pretty (Formatted (_, ExThis)) = pretty "ξ"-  pretty (Formatted (_, ExGlobal)) = pretty "Φ"-  pretty (Formatted (_, ExTermination)) = pretty "⊥"-  pretty (Formatted (_, ExMeta meta)) = prettyMeta meta-  pretty (Formatted (SWEET, ExApplication (ExApplication expr tau) tau')) =+  pretty (Formatted (SWEET, UNICODE, ExDispatch (ExDispatch ExGlobal (AtLabel "org")) (AtLabel "eolang"))) = pretty "Φ̇"+  pretty (Formatted (SWEET, ASCII, ExDispatch (ExDispatch ExGlobal (AtLabel "org")) (AtLabel "eolang"))) = pretty "QQ"+  pretty (Formatted (SWEET, _, DataString bytes)) = pretty "\"" <> pretty (btsToStr bytes) <> pretty "\""+  pretty (Formatted (SWEET, _, DataNumber bytes)) = either pretty pretty (btsToNum bytes)+  pretty (Formatted (SWEET, encoding, DataObject other bytes)) = pretty (Formatted (SALTY, encoding, DataObject other bytes))+  pretty (Formatted (SWEET, UNICODE, ExFormation [BiVoid AtRho])) = pretty "⟦⟧"+  pretty (Formatted (SWEET, ASCII, ExFormation [BiVoid AtRho])) = pretty "[[]]"+  pretty (Formatted (mode, encoding, ExFormation [binding])) = case binding of+    BiTau _ _ -> vsep [prettyLsb encoding, indent 2 (pretty (Formatted (mode, encoding, binding))), prettyRsb encoding]+    _ -> prettyLsb encoding <+> pretty (Formatted (mode, encoding, binding)) <+> prettyRsb encoding+  pretty (Formatted (_, UNICODE, ExFormation [])) = pretty "⟦⟧"+  pretty (Formatted (_, ASCII, ExFormation [])) = pretty "[[]]"+  pretty (Formatted (mode, encoding, ExFormation bindings)) = vsep [prettyLsb encoding, indent 2 (pretty (Formatted (mode, encoding, bindings))), prettyRsb encoding]+  pretty (Formatted (_, UNICODE, ExThis)) = pretty "ξ"+  pretty (Formatted (_, ASCII, ExThis)) = pretty "$"+  pretty (Formatted (_, UNICODE, ExGlobal)) = pretty "Φ"+  pretty (Formatted (_, ASCII, ExGlobal)) = pretty "Q"+  pretty (Formatted (_, UNICODE, ExTermination)) = pretty "⊥"+  pretty (Formatted (_, ASCII, ExTermination)) = pretty "T"+  pretty (Formatted (_, _, ExMeta meta)) = prettyMeta meta+  pretty (Formatted (SWEET, encoding, ExApplication (ExApplication expr tau) tau')) =     let (expr', taus, exprs) = complexApplication (ExApplication (ExApplication expr tau) tau')         args =           if null exprs-            then pretty (Formatted (SWEET, reverse taus))-            else vsep (punctuate comma (map (\exp -> pretty (Formatted (SWEET, exp))) (reverse exprs)))-     in pretty (Formatted (SWEET, expr')) <> vsep [lparen, indent 2 args, rparen]-  pretty (Formatted (SWEET, ExApplication expr tau)) =+            then pretty (Formatted (SWEET, encoding, reverse taus))+            else vsep (punctuate comma (map (\exp -> pretty (Formatted (SWEET, encoding, exp))) (reverse exprs)))+     in pretty (Formatted (SWEET, encoding, expr')) <> vsep [lparen, indent 2 args, rparen]+  pretty (Formatted (SWEET, encoding, ExApplication expr tau)) =     let arg = case tau of-          BiTau (AtAlpha 0) expr' -> pretty (Formatted (SWEET, expr'))-          _ -> pretty (Formatted (SWEET, tau))-     in pretty (Formatted (SWEET, expr)) <> vsep [lparen, indent 2 arg, rparen]-  pretty (Formatted (mode, ExApplication expr tau)) = pretty (Formatted (mode, expr)) <> vsep [lparen, indent 2 (pretty (Formatted (mode, tau))), rparen]-  pretty (Formatted (mode, ExDispatch expr attr)) = pretty (Formatted (mode, expr)) <> pretty "." <> pretty attr-  pretty (Formatted (mode, ExMetaTail expr meta)) = pretty (Formatted (mode, expr)) <+> pretty "*" <+> prettyMeta meta+          BiTau (AtAlpha 0) expr' -> pretty (Formatted (SWEET, encoding, expr'))+          _ -> pretty (Formatted (SWEET, encoding, tau))+     in pretty (Formatted (SWEET, encoding, expr)) <> vsep [lparen, indent 2 arg, rparen]+  pretty (Formatted (mode, encoding, ExApplication expr tau)) = pretty (Formatted (mode, encoding, expr)) <> vsep [lparen, indent 2 (pretty (Formatted (mode, encoding, tau))), rparen]+  pretty (Formatted (mode, encoding, ExDispatch expr attr)) = pretty (Formatted (mode, encoding, expr)) <> pretty "." <> pretty (Formatted (mode, encoding, attr))+  pretty (Formatted (mode, encoding, ExMetaTail expr meta)) = pretty (Formatted (mode, encoding, expr)) <+> pretty "*" <+> prettyMeta meta  instance Pretty (Formatted Program) where-  pretty (Formatted (SALTY, Program expr)) = pretty "Φ" <+> prettyArrow <+> pretty (Formatted (SALTY, expr))-  pretty (Formatted (SWEET, Program expr)) = pretty "{" <> pretty (Formatted (SWEET, expr)) <> pretty "}"+  pretty (Formatted (SALTY, encoding, Program expr)) = pretty "Φ" <+> prettyArrow encoding <+> pretty (Formatted (SALTY, encoding, expr))+  pretty (Formatted (SWEET, encoding, Program expr)) = pretty "{" <> pretty (Formatted (SWEET, encoding, expr)) <> pretty "}"  instance Pretty Tail where-  pretty (TaApplication tau) = vsep [lparen, indent 2 (pretty (Formatted (SALTY, tau))), rparen]-  pretty (TaDispatch attr) = pretty "." <> pretty attr+  pretty (TaApplication tau) = vsep [lparen, indent 2 (pretty (formatted tau)), rparen]+  pretty (TaDispatch attr) = pretty "." <> pretty (formatted attr)  instance Pretty MetaValue where-  pretty (MvAttribute attr) = pretty attr+  pretty (MvAttribute attr) = pretty (formatted attr)   pretty (MvBytes bytes) = pretty bytes   pretty (MvBindings []) = pretty "[]"-  pretty (MvBindings bindings) = vsep [pretty "[", indent 2 (pretty (Formatted (SALTY, bindings))), pretty "]"]+  pretty (MvBindings bindings) = vsep [pretty "[", indent 2 (pretty (formatted bindings)), pretty "]"]   pretty (MvFunction func) = pretty func-  pretty (MvExpression expr _) = pretty (Formatted (SALTY, expr))+  pretty (MvExpression expr _) = pretty (formatted expr)   pretty (MvTail tails) = vsep (punctuate comma (map pretty tails))  instance Pretty Subst where@@ -195,14 +224,14 @@       ]  instance {-# OVERLAPPING #-} Pretty (Formatted [Subst]) where-  pretty (Formatted (_, [])) = pretty "[]"-  pretty (Formatted (mode, substs)) = vsep [pretty "[", indent 2 (vsep (punctuate comma (map pretty substs))), pretty "]"]+  pretty (Formatted (_, _, [])) = pretty "[]"+  pretty (Formatted (mode, _, substs)) = vsep [pretty "[", indent 2 (vsep (punctuate comma (map pretty substs))), pretty "]"]  render :: (Pretty a) => a -> String render printable = renderString (layoutPretty defaultLayoutOptions (pretty printable))  prettyBinding :: Binding -> String-prettyBinding binding = render (Formatted (SALTY, binding))+prettyBinding binding = render (formatted binding)  prettyExtraArg :: ExtraArgument -> String prettyExtraArg = render@@ -211,7 +240,7 @@ prettyBytes = render  prettyAttribute :: Attribute -> String-prettyAttribute = render+prettyAttribute attr = render (formatted attr)  prettySubst :: Subst -> String prettySubst = render@@ -219,17 +248,17 @@ prettySubsts :: [Subst] -> String prettySubsts = render -prettySubsts' :: [Subst] -> PrintMode -> String-prettySubsts' substs mode = render (Formatted (mode, substs))+prettySubsts' :: [Subst] -> PrintMode -> Encoding -> String+prettySubsts' substs mode encoding = render (Formatted (mode, encoding, substs))  prettyExpression :: Expression -> String-prettyExpression expr = render (Formatted (SALTY, expr))+prettyExpression expr = render (formatted expr)  prettyExpression' :: Expression -> String-prettyExpression' expr = render (Formatted (SWEET, expr))+prettyExpression' expr = render (Formatted (SWEET, UNICODE, expr))  prettyProgram :: Program -> String-prettyProgram prog = render (Formatted (SALTY, prog))+prettyProgram prog = render (formatted prog) -prettyProgram' :: Program -> PrintMode -> String-prettyProgram' prog mode = render (Formatted (mode, prog))+prettyProgram' :: Program -> PrintMode -> Encoding -> String+prettyProgram' prog mode encoding = render (Formatted (mode, encoding, prog))
src/Rewriter.hs view
@@ -17,6 +17,7 @@ import Logger (logDebug) import Matcher (MetaValue (MvAttribute, MvBindings, MvBytes, MvExpression), Subst (Subst), combine, combineMany, defaultScope, matchProgram, substEmpty, substSingle) import Misc (ensuredFile)+import Must (Must (..), exceedsUpperBound, inRange) import Parser (parseProgram, parseProgramThrows) import Pretty (PrintMode (SWEET), prettyAttribute, prettyBytes, prettyExpression, prettyExpression', prettyProgram, prettyProgram', prettySubsts) import Replacer (ReplaceProgramContext (ReplaceProgramContext), ReplaceProgramThrowsFunc, replaceProgramFastThrows, replaceProgramThrows)@@ -31,27 +32,35 @@   { _program :: Program,     _maxDepth :: Integer,     _maxCycles :: Integer,+    _depthSensitive :: Bool,     _buildTerm :: BuildTermFunc,-    _must :: Integer+    _must :: Must   } -data MustException-  = StoppedBefore {must :: Integer, count :: Integer}-  | ContinueAfter {must :: Integer}+data RewriteException+  = MustBeGoing {must :: Must, count :: Integer}+  | MustStopBefore {must :: Must, count :: Integer}+  | StoppedOnLimit {flag :: String, limit :: Integer}   deriving (Exception) -instance Show MustException where-  show StoppedBefore {..} =+instance Show RewriteException where+  show MustBeGoing {..} =     printf-      "With option --must=%d it's expected exactly %d rewriting cycles happened, but rewriting stopped after %d"-      must-      must+      "With option --must=%s it's expected rewriting cycles to be in range [%s], but rewriting stopped after %d cycles"+      (show must)+      (show must)       count-  show ContinueAfter {..} =+  show MustStopBefore {..} =     printf-      "With option --must=%d it's expected exactly %d rewriting cycles happened, but rewriting is still going"-      must-      must+      "With option --must=%s it's expected rewriting cycles to be in range [%s], but rewriting has already reached %d cycles and is still going"+      (show must)+      (show must)+      count+  show StoppedOnLimit {..} =+    printf+      "With option --depth-sensitive it's expected rewriting iterations amount does not reach the limit: --%s=%d"+      flag+      limit  -- Build pattern and result expression and replace patterns to results in given program buildAndReplace' :: Expression -> Expression -> [Subst] -> ReplaceProgramThrowsFunc -> ReplaceProgramContext -> IO Program@@ -115,7 +124,9 @@        in if count - 1 == depth             then do               logDebug (printf "Max amount of rewriting cycles (%d) for rule '%s' has been reached, rewriting is stopped" depth ruleName)-              pure prog+              if _depthSensitive ctx+                then throwIO (StoppedOnLimit "max-depth" depth)+                else pure prog             else do               logDebug (printf "Starting rewriting cycle for rule '%s': %d out of %d" ruleName count depth)               matched <- R.matchProgramWithRule prog rule (RuleContext (_program ctx) (_buildTerm ctx))@@ -154,20 +165,23 @@     _rewrite prog count = do       let cycles = _maxCycles ctx           must = _must ctx-      if must /= 0 && count - 1 > must-        then throwIO (ContinueAfter must)+          current = count - 1+      if not (inRange must current) && current > 0 && exceedsUpperBound must current+        then throwIO (MustStopBefore must current)         else-          if count - 1 == cycles+          if current == cycles             then do               logDebug (printf "Max amount of rewriting cycles for all rules (%d) has been reached, rewriting is stopped" cycles)-              pure prog+              if _depthSensitive ctx+                then throwIO (StoppedOnLimit "max-cycles" cycles)+                else pure prog             else do               logDebug (printf "Starting rewriting cycle for all rules: %d out of %d" count cycles)               rewritten <- rewrite prog rules ctx               if rewritten == prog                 then do                   logDebug "No rule matched, rewriting is stopped"-                  if must /= 0 && count - 1 /= must-                    then throwIO (StoppedBefore must (count - 1))+                  if not (inRange must current)+                    then throwIO (MustBeGoing must current)                     else pure rewritten                 else _rewrite rewritten (count + 1)
src/Rule.hs view
@@ -19,7 +19,7 @@ import Logger (logDebug) import Matcher import Misc (allPathsIn, btsToUnescapedStr)-import Pretty (prettyAttribute, prettyBytes, prettyExpression, prettyExpression', prettySubsts)+import Pretty (prettyAttribute, prettyBytes, prettyExpression, prettyExpression', prettySubsts, prettyBinding) import Regexp (match) import Term (BuildTermFunc, Term (..)) import Text.Printf (printf)@@ -258,6 +258,9 @@                   TeBytes bytes -> do                     logDebug (printf "Function %s() returned bytes: %s" func (prettyBytes bytes))                     pure (MvBytes bytes)+                  TeBindings bds -> do+                    logDebug (printf "Function %s return bindings: %s" func (prettyExpression' (ExFormation bds)))+                    pure (MvBindings bds)                 case maybeName of                   Just name -> pure (combine (substSingle name meta) subst')                   _ -> pure Nothing
src/Term.hs view
@@ -13,7 +13,11 @@ import Matcher import Ast -data Term = TeExpression Expression | TeAttribute Attribute | TeBytes Bytes+data Term+  = TeExpression Expression+  | TeAttribute Attribute+  | TeBytes Bytes+  | TeBindings [Binding]  type BuildTermMethod = [ExtraArgument] -> Subst -> Program -> IO Term 
src/XMIR.hs view
@@ -6,7 +6,17 @@ -- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com -- SPDX-License-Identifier: MIT -module XMIR (programToXMIR, printXMIR, toName, parseXMIR, parseXMIRThrows, xmirToPhi, XmirContext (XmirContext)) where+module XMIR+  ( programToXMIR,+    printXMIR,+    toName,+    parseXMIR,+    parseXMIRThrows,+    xmirToPhi,+    defaultXmirContext,+    XmirContext (XmirContext),+  )+where  import Ast import Control.Exception (Exception (displayException), SomeException, throwIO)@@ -26,11 +36,11 @@ import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Data.Time.Format (defaultTimeLocale, formatTime) import Data.Version (showVersion)+import Debug.Trace import Misc import Paths_phino (version)-import Pretty (PrintMode, prettyAttribute, prettyBinding, prettyBytes, prettyExpression, prettyProgram')+import Pretty (PrintMode (SWEET), prettyAttribute, prettyBinding, prettyBytes, prettyExpression, prettyProgram) import Text.Printf (printf)-import Text.Read (readMaybe) import qualified Text.Read as TR import Text.XML import qualified Text.XML.Cursor as C@@ -38,22 +48,23 @@ data XmirContext = XmirContext   { omitListing :: Bool,     omitComments :: Bool,-    printMode :: PrintMode+    listing :: String   } --- @todo #116:30min Refactor XMIR module. This module became so big and hard to read.---  Now it's responsible for 3 different operations: 1) converting Phi AST to XML Document Ast,---  2) printing XML Document, 3) parsing XMIR to Phi AST. I think we should separate the logic---  in order to keep modules as little as possible.+defaultXmirContext :: XmirContext+defaultXmirContext = XmirContext True True ""+ data XMIRException-  = UnsupportedExpression {expr :: Expression}+  = UnsupportedProgram {prog :: Program}+  | UnsupportedExpression {expr :: Expression}   | UnsupportedBinding {binding :: Binding}   | CouldNotParseXMIR {message :: String}   | InvalidXMIRFormat {message :: String, cursor :: C.Cursor}   deriving (Exception)  instance Show XMIRException where-  show UnsupportedExpression {..} = printf "XMIR does not support such expression: %s" (prettyExpression expr)+  show UnsupportedProgram {..} = printf "XMIR does not support such program:\n%s" (prettyProgram prog)+  show UnsupportedExpression {..} = printf "XMIR does not support such expression:\n%s" (prettyExpression expr)   show UnsupportedBinding {..} = printf "XMIR does not support such bindings: %s" (prettyBinding binding)   show CouldNotParseXMIR {..} = printf "Couldn't parse given XMIR, cause: %s" message   show InvalidXMIRFormat {..} =@@ -78,9 +89,9 @@ object attrs children = NodeElement (element "o" attrs children)  -- @todo #278:30min Remove xmirAttributure and replace to prettyAttribute. Right now XMIR does not---  support "ρ" and "φ" in @base and @name attributes. When it's done, we should also generate +--  support "ρ" and "φ" in @base and @name attributes. When it's done, we should also generate --  such valid XMIR. To achieve that we should get rid of xmirAttribute function and use prettyAttribute---  instead. Also we should use "@" in formationBinding. Don't forget to remove the puzzle. +--  instead. Also we should use "@" in formationBinding. Don't forget to remove the puzzle. xmirAttribute :: Attribute -> String xmirAttribute AtPhi = "@" xmirAttribute AtRho = "^"@@ -141,9 +152,6 @@ expression (ExApplication (ExFormation bds) tau) _ = throwIO (UnsupportedExpression (ExApplication (ExFormation bds) tau)) expression expr _ = throwIO (UnsupportedExpression expr) -nestedBindings :: [Binding] -> XmirContext -> IO [Node]-nestedBindings bds ctx = catMaybes <$> mapM (`formationBinding` ctx) bds- formationBinding :: Binding -> XmirContext -> IO (Maybe Node) formationBinding (BiTau (AtLabel label) (ExFormation bds)) ctx = do   inners <- nestedBindings bds ctx@@ -162,86 +170,102 @@ formationBinding (BiVoid (AtLabel label)) _ = pure (Just (object [("name", label), ("base", "∅")] [])) formationBinding binding _ = throwIO (UnsupportedBinding binding) -rootExpression :: Expression -> XmirContext -> IO Node-rootExpression (ExFormation [bd, BiVoid AtRho]) ctx = do-  [bd'] <- nestedBindings [bd] ctx-  pure bd'-rootExpression expr _ = throwIO (UnsupportedExpression expr)---- Extract package from given expression--- The function returns tuple (X, Y), where--- - X: list of package parts--- - Y: root object expression-getPackage :: Expression -> IO ([String], Expression)-getPackage (ExFormation [BiTau (AtLabel label) (ExFormation [bd, BiLambda "Package", BiVoid AtRho]), BiVoid AtRho]) = do-  (pckg, expr') <- getPackage (ExFormation [bd, BiLambda "Package", BiVoid AtRho])-  pure (label : pckg, expr')-getPackage (ExFormation [BiTau (AtLabel label) (ExFormation [bd, BiLambda "Package", BiVoid AtRho]), BiLambda "Package", BiVoid AtRho]) = do-  (pckg, expr') <- getPackage (ExFormation [bd, BiLambda "Package", BiVoid AtRho])-  pure (label : pckg, expr')-getPackage (ExFormation [BiTau attr expr, BiLambda "Package", BiVoid AtRho]) = pure ([], ExFormation [BiTau attr expr, BiVoid AtRho])-getPackage (ExFormation [bd, BiVoid AtRho]) = pure ([], ExFormation [bd, BiVoid AtRho])-getPackage expr = throwIO (userError (printf "Can't extract package from given expression:\n %s" (prettyExpression expr)))+nestedBindings :: [Binding] -> XmirContext -> IO [Node]+nestedBindings bds ctx = catMaybes <$> mapM (`formationBinding` ctx) bds -metasWithPackage :: String -> Node-metasWithPackage pckg =-  NodeElement-    ( element-        "metas"-        []-        [ NodeElement+programToXMIR :: Program -> XmirContext -> IO Document+programToXMIR prog@(Program expr@(ExFormation [BiTau (AtLabel _) arg, BiVoid AtRho])) ctx@XmirContext {..} = case arg of+  ExFormation _ -> programToXMIR'+  ExApplication _ _ -> programToXMIR'+  ExDispatch _ _ -> programToXMIR'+  ExGlobal -> programToXMIR'+  _ -> throwIO (UnsupportedProgram prog)+  where+    programToXMIR' :: IO Document+    programToXMIR' = do+      (pckg, expr') <- getPackage expr+      root <- rootExpression expr' ctx+      now <- getCurrentTime+      let listingContent =+            if omitListing+              then show (length (lines listing)) ++ " line(s)"+              else listing+          listing' = NodeElement (element "listing" [] [NodeContent (T.pack listingContent)])+          metas = metasWithPackage (intercalate "." pckg)+      pure+        ( Document+            (Prologue [] Nothing [])             ( element-                "meta"-                []-                [ NodeElement (element "head" [] [NodeContent (T.pack "package")]),-                  NodeElement (element "tail" [] [NodeContent (T.pack pckg)]),-                  NodeElement (element "part" [] [NodeContent (T.pack pckg)])+                "object"+                [ ("dob", formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S" now),+                  ("ms", "0"),+                  ("revision", "1234567"),+                  ("time", time now),+                  ("version", showVersion version),+                  ("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"),+                  ("xsi:noNamespaceSchemaLocation", "https://raw.githubusercontent.com/objectionary/eo/refs/heads/gh-pages/XMIR.xsd")                 ]+                ( if null pckg+                    then [listing', root]+                    else [listing', metas, root]+                )             )-        ]-    )--time :: UTCTime -> String-time now = do-  let base = formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S" now-      posix = utcTimeToPOSIXSeconds now-      fractional :: Double-      fractional = realToFrac posix - fromInteger (floor posix)-      nanos = floor (fractional * 1_000_000_000) :: Int-  base ++ "." ++ printf "%09d" nanos ++ "Z"--programToXMIR :: Program -> XmirContext -> IO Document-programToXMIR (Program expr) ctx = do-  (pckg, expr') <- getPackage expr-  root <- rootExpression expr' ctx-  now <- getCurrentTime-  let phi = prettyProgram' (Program expr) (printMode ctx)-      listing =-        if omitListing ctx-          then show (length (lines phi)) ++ " lines of phi"-          else phi-      listing' = NodeElement (element "listing" [] [NodeContent (T.pack listing)])-      metas = metasWithPackage (intercalate "." pckg)-  pure-    ( Document-        (Prologue [] Nothing [])+            []+        )+    -- Extract package from given expression+    -- The function returns tuple (X, Y), where+    -- - X: list of package parts+    -- - Y: root object expression+    getPackage :: Expression -> IO ([String], Expression)+    getPackage (ExFormation [BiTau (AtLabel label) (ExFormation [bd, BiLambda "Package", BiVoid AtRho]), BiVoid AtRho]) = do+      (pckg, expr') <- getPackage (ExFormation [bd, BiLambda "Package", BiVoid AtRho])+      pure (label : pckg, expr')+    getPackage (ExFormation [BiTau (AtLabel label) (ExFormation [bd, BiLambda "Package", BiVoid AtRho]), BiLambda "Package", BiVoid AtRho]) = do+      (pckg, expr') <- getPackage (ExFormation [bd, BiLambda "Package", BiVoid AtRho])+      pure (label : pckg, expr')+    getPackage (ExFormation [BiTau attr expr, BiLambda "Package", BiVoid AtRho]) = pure ([], ExFormation [BiTau attr expr, BiVoid AtRho])+    getPackage (ExFormation [bd, BiVoid AtRho]) = pure ([], ExFormation [bd, BiVoid AtRho])+    getPackage expr = throwIO (userError (printf "Can't extract package from given expression:\n %s" (prettyExpression expr)))+    -- Convert root Expression to Node+    rootExpression :: Expression -> XmirContext -> IO Node+    rootExpression (ExFormation [bd, BiVoid AtRho]) ctx = do+      [bd'] <- nestedBindings [bd] ctx+      pure bd'+    rootExpression expr _ = throwIO (UnsupportedExpression expr)+    -- Returns metas Node with package:+    -- <metas>+    --   <meta>+    --     <head>package</head>+    --     <tail><!-- package here --></tail>+    --     <part><!-- package here --></part>+    --   </meta>+    -- </metas>+    metasWithPackage :: String -> Node+    metasWithPackage pckg =+      NodeElement         ( element-            "object"-            [ ("dob", formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S" now),-              ("ms", "0"),-              ("revision", "1234567"),-              ("time", time now),-              ("version", showVersion version),-              ("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"),-              ("xsi:noNamespaceSchemaLocation", "https://raw.githubusercontent.com/objectionary/eo/refs/heads/gh-pages/XMIR.xsd")+            "metas"+            []+            [ NodeElement+                ( element+                    "meta"+                    []+                    [ NodeElement (element "head" [] [NodeContent (T.pack "package")]),+                      NodeElement (element "tail" [] [NodeContent (T.pack pckg)]),+                      NodeElement (element "part" [] [NodeContent (T.pack pckg)])+                    ]+                )             ]-            ( if null pckg-                then [listing', root]-                else [listing', metas, root]-            )         )-        []-    )+    time :: UTCTime -> String+    time now = do+      let base = formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S" now+          posix = utcTimeToPOSIXSeconds now+          fractional :: Double+          fractional = realToFrac posix - fromInteger (floor posix)+          nanos = floor (fractional * 1_000_000_000) :: Int+      base ++ "." ++ printf "%09d" nanos ++ "Z"+programToXMIR prog _ = throwIO (UnsupportedProgram prog)  -- Add indentation (2 spaces per level). indent :: Int -> TB.Builder@@ -361,7 +385,7 @@   | not (hasAttr "name" cur) = throwIO (InvalidXMIRFormat "Formation children must have @name attribute" cur)   | not (hasAttr "base" cur) = do       name <- getAttr "name" cur-      bds <- mapM (`xmirToFormationBinding` (name : fqn)) (cur C.$/ C.element (toName "o"))+      bds <- mapM (`xmirToFormationBinding` (name : fqn)) (cur C.$/ C.element (toName "o")) >>= uniqueBindings'       case name of         "λ" -> pure (BiLambda (intercalate "_" ("L" : reverse fqn)))         ('α' : _) -> throwIO (InvalidXMIRFormat "Formation child @name can't start with α" cur)@@ -409,7 +433,7 @@         '$' : '.' : rest -> xmirToExpression' ExThis "$" rest cur fqn         _ -> throwIO (InvalidXMIRFormat "The @base attribute must be either ['∅'|'Q'] or start with ['Q.'|'$.'|'.']" cur)   | otherwise = do-      bds <- mapM (`xmirToFormationBinding` fqn) (cur C.$/ C.element (toName "o"))+      bds <- mapM (`xmirToFormationBinding` fqn) (cur C.$/ C.element (toName "o")) >>= uniqueBindings'       pure (ExFormation (withVoidRho bds))   where     xmirToExpression' :: Expression -> String -> String -> C.Cursor -> [String] -> IO Expression@@ -422,8 +446,7 @@               (\acc part -> ExDispatch acc <$> toAttr (T.unpack part) cur)               start               (T.splitOn "." (T.pack rest))-          let args = cur C.$/ C.element (toName "o")-          xmirToApplication head' args fqn+          xmirToApplication head' (cur C.$/ C.element (toName "o")) fqn  xmirToApplication :: Expression -> [C.Cursor] -> [String] -> IO Expression xmirToApplication = xmirToApplication' 0
src/Yaml.hs view
@@ -17,7 +17,7 @@ import Data.Yaml (Parser) import qualified Data.Yaml as Yaml import GHC.Generics-import Misc (allPathsIn)+import Misc (allPathsIn, validateYamlObject) import Parser  parseJSON' :: String -> (String -> Either String a) -> Value -> Parser a@@ -52,7 +52,8 @@  instance FromJSON Number where   parseJSON v = case v of-    Object o ->+    Object o -> do+      validateYamlObject o ["ordinal", "length"]       asum         [ Ordinal <$> o .: "ordinal",           Length <$> o .: "length"@@ -73,7 +74,8 @@   parseJSON =     withObject       "Condition"-      ( \v ->+      ( \v -> do+          validateYamlObject v ["and", "or", "not", "alpha", "nf", "xi", "eq", "in", "matches", "part-of"]           asum             [ And <$> v .: "and",               Or <$> v .: "or",
test/BuilderSpec.hs view
@@ -8,7 +8,8 @@ import Control.Monad import Data.Map.Strict qualified as Map import Matcher-import Test.Hspec (Example (Arg), Expectation, Spec, SpecWith, anyException, describe, it, shouldBe, shouldThrow)+import Test.Hspec (Example (Arg), Expectation, Spec, SpecWith, anyException, describe, it, shouldBe, shouldThrow, shouldSatisfy)+import Data.Either (isLeft)  test :: (Show a, Eq a) => (a -> Subst -> Either String (a, a)) -> [(String, a, [(String, MetaValue)], Either String (a, a))] -> SpecWith (Arg Expectation) test function useCases =@@ -50,7 +51,7 @@         ( "Q.!a => () => X",           ExDispatch ExGlobal (AtMeta "a"),           [],-          Left "a"+          Left "meta 'a' is either does not exist or refers to an inappropriate term"         ),         ( "!e0(!a1 -> !e1, !a2 => !e2) => (!e0 >> [[]], !a1 >> x, !e1 >> Q, !a2 >> y, !e2 >> $) => [[]](x -> Q, y -> $)",           ExApplication (ExApplication (ExMeta "e0") (BiTau (AtMeta "a1") (ExMeta "e1"))) (BiTau (AtMeta "a2") (ExMeta "e2")),@@ -94,3 +95,9 @@         (ExMeta "e")         [substSingle "e1" (MvExpression (ExDispatch ExGlobal (AtLabel "x")) defaultScope)]         `shouldThrow` anyException+  +  describe "build with duplicate attributes in bindings" $ do+    it "build binding with duplicates" $+      buildBinding (BiMeta "B") (substSingle "B" (MvBindings [BiVoid AtRho, BiVoid AtRho])) `shouldSatisfy` isLeft+    it "build formation with duplicates" $+      buildExpression (ExMeta "e") (substSingle "e" (MvExpression (ExFormation [BiVoid AtRho, BiVoid AtRho]) ExThis)) `shouldSatisfy` isLeft
test/CLISpec.hs view
@@ -16,7 +16,6 @@ import System.Directory (removeFile) import System.Exit (ExitCode (ExitFailure)) import System.IO-import System.IO.Silently (capture_) import Test.Hspec import Text.Printf (printf) @@ -64,6 +63,12 @@   where     cleanup (fp, _) = removeFile fp +withTempFile :: String -> ((FilePath, Handle) -> IO a) -> IO a+withTempFile pattern =+  bracket+    (openTempFile "." pattern)+    (\(path, _) -> removeFile path)+ testCLI :: [String] -> [String] -> Expectation testCLI args outputs = do   (out, _) <- withStdout (try (runCLI args) :: IO (Either ExitCode ()))@@ -86,24 +91,24 @@   it "prints version" $     testCLI ["--version"] [showVersion version] -  it "prints help" $ do-    output <- capture_ (runCLI ["--help"])-    output `shouldContain` "Phino - CLI Manipulator of 𝜑-Calculus Expressions"-    output `shouldContain` "Usage:"+  it "prints help" $+    testCLI+      ["--help"]+      ["Phino - CLI Manipulator of 𝜑-Calculus Expressions", "Usage:"]    it "prints debug info with --log-level=DEBUG" $     withStdin "Q -> [[]]" $-      testCLI ["rewrite", "--nothing", "--log-level=DEBUG"] ["[DEBUG]:"]+      testCLI ["rewrite", "--log-level=DEBUG"] ["[DEBUG]:"]    describe "rewrites" $ do-    it "desugares with --nothing flag from file" $+    it "desugares without any rules flag from file" $       testCLI-        ["rewrite", "--nothing", "test-resources/cli/desugar.phi"]+        ["rewrite", "test-resources/cli/desugar.phi"]         ["Φ ↦ ⟦\n  foo ↦ Φ.org.eolang,\n  ρ ↦ ∅\n⟧"] -    it "desugares with --nothing flag from stdin" $+    it "desugares with without any rules flag from stdin" $       withStdin "{[[foo ↦ QQ]]}" $-        testCLI ["rewrite", "--nothing"] ["Φ ↦ ⟦\n  foo ↦ Φ.org.eolang,\n  ρ ↦ ∅\n⟧"]+        testCLI ["rewrite"] ["Φ ↦ ⟦\n  foo ↦ Φ.org.eolang,\n  ρ ↦ ∅\n⟧"]      it "rewrites with single rule" $       withStdin "{T(x -> Q.y)}" $@@ -125,13 +130,13 @@             ]         ] -    it "fails with negative --max-depth" $+    it "fails with --input=latex" $       withStdin "" $-        testCLIFailed ["rewrite", "--max-depth=-1"] "--max-depth must be positive"+        testCLIFailed ["rewrite", "--input=latex"] "The value 'latex' can't be used for '--input' option" -    it "fails with no rewriting options provided" $+    it "fails with negative --max-depth" $       withStdin "" $-        testCLIFailed ["rewrite"] "no --rule, no --normalize, no --nothing are provided"+        testCLIFailed ["rewrite", "--max-depth=-1"] "--max-depth must be positive"      it "normalizes from stdin" $       withStdin "Φ ↦ ⟦ a ↦ ⟦ b ↦ ∅ ⟧ (b ↦ [[ ]]) ⟧" $@@ -151,19 +156,37 @@     it "rewrites with --sweet flag" $       withStdin "Q -> [[ x -> 5]]" $         testCLI-          ["rewrite", "--nothing", "--sweet"]+          ["rewrite", "--sweet"]           ["{⟦\n  x ↦ 5\n⟧}"]      it "rewrites as XMIR" $       withStdin "Q -> [[ x -> Q.y ]]" $         testCLI-          ["rewrite", "--nothing", "--output=xmir"]+          ["rewrite", "--output=xmir"]           ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<object", "  <o base=\"Q.y\" name=\"x\"/>"] +    it "rewrites as LaTeX" $+      withStdin "Q -> [[ x -> QQ.z(y -> 5), q -> T ]]" $+        testCLI+          ["rewrite", "--output=latex", "--sweet"]+          [ "\\documentclass{article}",+            "\\usepackage{eolang}",+            "\\begin{document}",+            "\\begin{phiquation}",+            "{[[",+            "  x -> QQ.z(",+            "    y -> 5",+            "  ),",+            "  q -> T",+            "]]}",+            "\\end{phiquation}",+            "\\end{document}"+          ]+     it "rewrites with XMIR as input" $       withStdin "<object><o name=\"app\"><o name=\"x\" base=\"Q.number\"/></o></object>" $         testCLI-          ["rewrite", "--nothing", "--input=xmir", "--sweet"]+          ["rewrite", "--input=xmir", "--sweet"]           [ unlines               [ "{⟦",                 "  app ↦ ⟦",@@ -176,8 +199,8 @@     it "rewrites as XMIR with omit-listing flag" $       withStdin "Q -> [[ x -> Q.y ]]" $         testCLI-          ["rewrite", "--nothing", "--output=xmir", "--omit-listing"]-          ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<object", "<listing>4 lines of phi</listing>", "  <o base=\"Q.y\" name=\"x\"/>"]+          ["rewrite", "--output=xmir", "--omit-listing"]+          ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<object", "<listing>1 line(s)</listing>", "  <o base=\"Q.y\" name=\"x\"/>"]      it "does not fail on exactly 1 rewriting" $       withStdin "{⟦ t ↦ ⟦ x ↦ \"foo\" ⟧ ⟧}" $@@ -185,41 +208,122 @@           ["rewrite", "--rule=test-resources/cli/simple.yaml", "--must=1", "--sweet"]           ["x ↦ \"bar\""] -    it "fails with --nothing and --must" $-      withStdin "Q -> [[ ]]" $-        testCLIFailed ["rewrite", "--nothing", "--must"] "it's expected exactly 1 rewriting cycles happened, but rewriting stopped after 0"--    it "fails with --normalize and --must" $+    it "fails with --normalize and --must=1" $       withStdin "Q -> [[ x -> [[ y -> 5 ]].y ]].x" $-        testCLIFailed ["rewrite", "--max-depth=2", "--normalize", "--must"] "it's expected exactly 1 rewriting cycles happened, but rewriting is still going"+        testCLIFailed ["rewrite", "--max-depth=2", "--normalize", "--must=1"] "it's expected rewriting cycles to be in range [1], but rewriting has already reached 2" +    describe "must range tests" $ do+      it "accepts range ..5 (0 to 5 cycles)" $+        withStdin "Q -> [[ ]]" $+          testCLI ["rewrite", "--must=..5", "--sweet"] ["{⟦⟧}"]++      it "accepts range 0..0 (exactly 0 cycles)" $+        withStdin "Q -> [[ ]]" $+          testCLI ["rewrite", "--must=0..0", "--sweet"] ["{⟦⟧}"]++      it "accepts range 1..1 (exactly 1 cycle)" $+        withStdin "{⟦ t ↦ ⟦ x ↦ \"foo\" ⟧ ⟧}" $+          testCLI+            ["rewrite", "--rule=test-resources/cli/simple.yaml", "--must=1..1", "--sweet"]+            ["x ↦ \"bar\""]++      it "accepts range 1..3 when 1 cycle happens" $+        withStdin "{⟦ t ↦ ⟦ x ↦ \"foo\" ⟧ ⟧}" $+          testCLI+            ["rewrite", "--rule=test-resources/cli/simple.yaml", "--must=1..3", "--sweet"]+            ["x ↦ \"bar\""]++      it "accepts range 0.. (0 or more)" $+        withStdin "Q -> [[ ]]" $+          testCLI ["rewrite", "--must=0..", "--sweet"] ["{⟦⟧}"]++      it "fails when cycles exceed range ..1" $+        withStdin "Q -> [[ x -> [[ y -> 5 ]].y ]].x" $+          testCLIFailed+            ["rewrite", "--max-depth=2", "--normalize", "--must=..1"]+            "it's expected rewriting cycles to be in range [..1], but rewriting has already reached 2"++      it "fails when cycles below range 2.." $+        withStdin "{⟦ t ↦ ⟦ x ↦ \"foo\" ⟧ ⟧}" $+          testCLIFailed+            ["rewrite", "--rule=test-resources/cli/simple.yaml", "--must=2.."]+            "it's expected rewriting cycles to be in range [2..], but rewriting stopped after 1"++      it "fails with invalid range 5..3" $+        withStdin "Q -> [[ ]]" $+          testCLIFailed+            ["rewrite", "--must=5..3"]+            "cannot parse value `5..3'"++      it "fails with negative in range -1..5" $+        withStdin "Q -> [[ ]]" $+          testCLIFailed+            ["rewrite", "--must=-1..5"]+            "cannot parse value `-1..5'"++      it "fails with malformed range syntax" $+        withStdin "Q -> [[ ]]" $+          testCLIFailed+            ["rewrite", "--must=3...5"]+            "cannot parse value `3...5'"+     it "prints to target file" $       withStdin "Q -> [[ ]]" $-        bracket-          (openTempFile "." "targetXXXXXX.tmp")-          (\(path, _) -> removeFile path)-          ( \(path, h) -> do-              hClose h-              testCLI-                ["rewrite", "--nothing", "--sweet", printf "--target=%s" path]-                [printf "The result program was saved in '%s'" path]-              content <- readFile path-              content `shouldBe` "{⟦⟧}"-          )+        withTempFile "targetXXXXXX.tmp" $ \(path, h) -> do+          hClose h+          testCLI+            ["rewrite", "--sweet", printf "--target=%s" path]+            [printf "The command result was saved in '%s'" path]+          content <- readFile path+          content `shouldBe` "{⟦⟧}" +    it "modifies file in-place" $+      withTempFile "inplaceXXXXXX.phi" $ \(path, h) -> do+        hPutStr h "Q -> [[ x -> \"foo\" ]]"+        hClose h+        testCLI+          ["rewrite", "--rule=test-resources/cli/simple.yaml", "--in-place", "--sweet", path]+          [printf "The file '%s' was modified in-place" path]+        content <- readFile path+        content `shouldBe` "{⟦\n  x ↦ \"bar\"\n⟧}"++    it "fails when --in-place is used without input file" $+      withStdin "Q -> [[ ]]" $+        testCLIFailed+          ["rewrite", "--in-place"]+          "--in-place requires an input file"++    it "fails when --in-place is used with --target" $+      withTempFile "inplaceXXXXXX.phi" $ \(path, h) -> do+        hPutStr h "Q -> [[ ]]"+        hClose h+        testCLIFailed+          ["rewrite", "--in-place", "--target=output.phi", path]+          "--in-place and --target cannot be used together"+     it "rewrites with cycles" $-      withStdin "Q -> [[ x -> 1 ]]" $+      withStdin "Q -> [[ x -> \"x\" ]]" $         testCLI           ["rewrite", "--sweet", "--rule=test-resources/cli/infinite.yaml", "--max-depth=1", "--max-cycles=2"]           [ unlines               [ "{⟦",-                "  x ↦ 1,",-                "  x ↦ 1,",-                "  x ↦ 1",+                "  x ↦ \"x_hi_hi\"",                 "⟧}"               ]           ] +    it "fails with --depth-sensitive" $+      withStdin "Q -> [[ x -> \"x\"]]" $+        testCLIFailed+          ["rewrite", "--depth-sensitive", "--max-depth=1", "--max-cycles=1", "--rule=test-resources/cli/infinite.yaml"]+          "[ERROR]: With option --depth-sensitive it's expected rewriting iterations amount does not reach the limit: --max-depth=1"++    it "fails on --sweet and --output=xmir together" $+      withStdin "Q -> [[ ]]" $+        testCLIFailed+          ["rewrite", "--sweet", "--output=xmir"]+          "The --sweet and --output=xmir can't stay together"+   describe "dataize" $ do     it "dataizes simple program" $       withStdin "Q -> [[ D> 01- ]]" $@@ -228,3 +332,38 @@     it "fails to dataize" $       withStdin "Q -> [[ ]]" $         testCLIFailed ["dataize"] "[ERROR]: Could not dataize given program"++  describe "explain" $ do+    it "explains single rule" $+      testCLI+        ["explain", "--rule=resources/copy.yaml"]+        ["\\documentclass{article}", "\\usepackage{amsmath}", "\\begin{document}", "\\rule{COPY}", "\\end{document}"]++    it "explains multiple rules" $+      testCLI+        ["explain", "--rule=resources/copy.yaml", "--rule=resources/alpha.yaml"]+        ["\\documentclass{article}", "\\rule{COPY}", "\\rule{ALPHA}"]++    it "explains normalization rules" $+      testCLI+        ["explain", "--normalize"]+        ["\\documentclass{article}", "\\begin{document}", "\\end{document}"]++    it "fails with no rules specified" $+      testCLIFailed+        ["explain"]+        "Either --rule or --normalize must be specified"++    it "writes to target file" $+      bracket+        (openTempFile "." "explainXXXXXX.tex")+        (\(path, _) -> removeFile path)+        ( \(path, h) -> do+            hClose h+            testCLI+              ["explain", "--normalize", printf "--target=%s" path]+              [printf "was saved in '%s'" path]+            content <- readFile path+            content `shouldContain` "\\documentclass{article}"+            content `shouldContain` "\\begin{document}"+        )
test/DataizeSpec.hs view
@@ -11,7 +11,7 @@ import Functions (buildTerm)  defaultDataizeContext :: Program -> DataizeContext-defaultDataizeContext prog = DataizeContext prog 25 1 buildTerm+defaultDataizeContext prog = DataizeContext prog 25 1 False buildTerm  test :: (Eq a, Show a) => (Expression -> DataizeContext -> IO (Maybe a)) -> [(String, Expression, Expression, Maybe a)] -> Spec test func useCases =
+ test/FunctionsSpec.hs view
@@ -0,0 +1,28 @@+-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com+-- SPDX-License-Identifier: MIT++module FunctionsSpec where++import Ast+import Data.Map.Strict qualified as Map+import Functions (buildTerm)+import Logger (logDebug)+import Matcher (MetaValue (MvBindings), Subst (Subst))+import Misc (uniqueBindings')+import Pretty (prettyExpression')+import Term (Term (TeBindings))+import Test.Hspec (Spec, describe, it, shouldBe)+import Text.Printf (printf)+import Yaml (ExtraArgument (ArgBinding))++spec :: Test.Hspec.Spec+spec = describe "Functions" $+  Test.Hspec.it "contains only unique bindings after 'join'" $ do+    let first = ("B1", MvBindings [BiVoid AtRho, BiDelta BtEmpty, BiTau (AtLabel "x") ExGlobal, BiVoid (AtAlpha 0)])+        second = ("B2", MvBindings [BiTau AtRho ExThis, BiLambda "Func", BiDelta (BtOne "00"), BiVoid (AtAlpha 1)])+        third = ("B3", MvBindings [BiLambda "Some", BiTau (AtLabel "y") ExThis, BiTau (AtLabel "x") ExThis, BiVoid (AtAlpha 0)])+        subst = Subst (Map.fromList [first, second, third])+    TeBindings bds <- buildTerm "join" [ArgBinding (BiMeta "B1"), ArgBinding (BiMeta "B2"), ArgBinding (BiMeta "B3")] subst (Program ExGlobal)+    bds' <- uniqueBindings' bds+    logDebug (printf "Joined bindings:\n%s" (prettyExpression' (ExFormation bds')))+    length bds' `shouldBe` 9
test/MiscSpec.hs view
@@ -4,9 +4,10 @@ module MiscSpec where  import Control.Monad (forM_)-import Misc (withVoidRho)-import Test.Hspec (Example (Arg), Expectation, Spec, SpecWith, describe, it, shouldBe)+import Misc (withVoidRho, uniqueBindings)+import Test.Hspec (Example (Arg), Expectation, Spec, SpecWith, describe, it, shouldBe, shouldSatisfy) import Ast+import Data.Either (isLeft, isRight)  testWithVoidRho :: [(String, [Binding], [Binding])] -> SpecWith (Arg Expectation) testWithVoidRho useCases =@@ -14,7 +15,7 @@     it desc $ withVoidRho before `shouldBe` after  spec :: Spec-spec =+spec = do   describe "with void rho binding" $     testWithVoidRho       [ ( "[[x -> ?]] => [[x -> ?, ^ -> ?]]",@@ -48,3 +49,9 @@           [BiTau (AtMeta "a") (ExDispatch ExGlobal (AtLabel "x")), BiTau AtRho (ExDispatch ExTermination (AtLabel "y"))]         )       ]++  describe "unique bindings" $ do+    it "fails with duplicate attribute" $+      uniqueBindings [BiVoid AtRho, BiVoid AtRho] `shouldSatisfy` isLeft+    it "does not fail on different attributes" $+      uniqueBindings [BiVoid AtPhi, BiVoid AtRho] `shouldSatisfy` isRight
test/ParserSpec.hs view
@@ -99,16 +99,13 @@                 )             )         ),-        ( "[[x -> y.z, a -> ~1, w -> ^, u -> @, p -> !a, q -> !e]]",+        ( "[[x -> y.z, w -> ^, u -> @, p -> !a, q -> !e]]",           Just             ( ExFormation                 [ BiTau                     (AtLabel "x")                     (ExDispatch (ExDispatch ExThis (AtLabel "y")) (AtLabel "z")),                   BiTau-                    (AtLabel "a")-                    (ExDispatch ExThis (AtAlpha 1)),-                  BiTau                     (AtLabel "w")                     (ExDispatch ExThis AtRho),                   BiTau@@ -124,21 +121,18 @@                 ]             )         ),-        ( "Q.x(~1, y, [[]].z, Q.y(^,@))",+        ( "Q.x(y, [[]].z, Q.y(^,@))",           Just             ( ExApplication                 ( ExApplication                     ( ExApplication-                        ( ExApplication-                            (ExDispatch ExGlobal (AtLabel "x"))-                            (BiTau (AtAlpha 0) (ExDispatch ExThis (AtAlpha 1)))-                        )-                        (BiTau (AtAlpha 1) (ExDispatch ExThis (AtLabel "y")))+                        (ExDispatch ExGlobal (AtLabel "x"))+                        (BiTau (AtAlpha 0) (ExDispatch ExThis (AtLabel "y")))                     )-                    (BiTau (AtAlpha 2) (ExDispatch (ExFormation [BiVoid AtRho]) (AtLabel "z")))+                    (BiTau (AtAlpha 1) (ExDispatch (ExFormation [BiVoid AtRho]) (AtLabel "z")))                 )                 ( BiTau-                    (AtAlpha 3)+                    (AtAlpha 2)                     ( ExApplication                         ( ExApplication                             (ExDispatch ExGlobal (AtLabel "y"))@@ -229,17 +223,15 @@         "Q.x(y() -> [[]])",         "Q.x(y(q) -> [[w -> !e]])",         "Q.x(~1(^,@) -> [[]])",-        "Q.x.~1.^.@.!a0",+        "Q.x.^.@.!a0",         "[[x -> y.z]]",-        "[[x -> ~1]]",         "[[x -> ^, y -> @, z -> !a]]",         "Q.x(a.b.c, Q.a(b), [[]])",-        "Q.x(~1, y, [[]].z, Q.y(^,@))",+        "Q.x(y, [[]].z, Q.y(^,@))",         "[[x -> 5.plus(5), y -> \"hello\", z -> 42.5]]",-        "[[w -> x(~1)]]",         "[[\n  x -> \"Hi\",\n  y -> 42\n]]",         "[[x -> -42, y -> +34]]",-        "⟦x ↦ Φ.org.eolang(z ↦ ξ.f, x ↦ α0, φ ↦ ρ, t ↦ φ, first ↦ ⟦ λ ⤍ Function_name, Δ ⤍ 42- ⟧)⟧",+        "⟦x ↦ Φ.org.eolang(z ↦ ξ.f, φ ↦ ρ, t ↦ φ, first ↦ ⟦ λ ⤍ Function_name, Δ ⤍ 42- ⟧)⟧",         "[[x -> 1.00e+3, y -> 2.32e-4]]",         "[[ x -> \"\\u0001\\u0001\"]]",         "[[ x -> \"\\uD835\\uDF11\"]]",@@ -269,11 +261,15 @@             "[[z(w) -> Q.x]]",             "Q.x(y(~1) -> [[]])",             "Q.x(1, 2, !B)",+            "Q.x.~0",             "Q.x(~1 -> Q.y, x -> 5, !B1)",             "Q.x(𝐵1, 𝜏0 -> $, x -> 𝑒)",             "[[ x -> \"\\uD800\"]]",             "[[ x -> \"\\uDFFF\"]]",-            "[[ x -> \"\\uD835\\u0041\"]]"+            "[[ x -> \"\\uD835\\u0041\"]]",+            "[[ x -> 1, x -> 2 ]]",+            "⟦ k ↦ ⟦ λ ⤍ Foo, λ ⤍ Bar ⟧ ⟧",+            "⟦ k ↦ ⟦ Δ ⤍ 42-, Δ ⤍ 55- ⟧ ⟧"           ]       ) @@ -284,4 +280,13 @@       ( \pack -> do           content <- runIO (readFile pack)           it (takeBaseName pack) (parseProgram content `shouldSatisfy` isRight)+      )++  describe "process typo packs" $ do+    packs <- runIO (allPathsIn "test-resources/phi-typos-packs")+    forM_+      packs+      ( \pack -> do+          content <- runIO (readFile pack)+          it (takeBaseName pack) (parseProgram content `shouldSatisfy` isLeft)       )
test/PrettySpec.hs view
@@ -10,10 +10,10 @@ import Pretty import Test.Hspec (Example (Arg), Expectation, Spec, SpecWith, describe, it, runIO, shouldBe) -test :: (a -> PrintMode -> String) -> [(String, String, a, PrintMode)] -> SpecWith (Arg Expectation)+test :: (a -> PrintMode -> Encoding -> String) -> [(String, String, a, PrintMode)] -> SpecWith (Arg Expectation) test function useCases =   forM_ useCases $ \(input, output, arg, mode) ->-    it input $ function arg mode `shouldBe` output+    it input $ function arg mode UNICODE `shouldBe` output  prep :: PrintMode -> (String, String) -> IO (String, String, Program, PrintMode) prep mode (input, output) = do
test/RewriterSpec.hs view
@@ -15,8 +15,9 @@ import Functions (buildTerm) import GHC.Generics import Misc (allPathsIn, ensuredFile)+import Must (Must(..)) import Parser (parseProgramThrows)-import Pretty (PrintMode (SWEET), prettyProgram')+import Pretty (PrintMode (SWEET), prettyProgram', Encoding (UNICODE)) import Rewriter (RewriteContext (RewriteContext), rewrite') import System.FilePath (makeRelative, replaceExtension, (</>)) import Test.Hspec (Spec, describe, expectationFailure, it, pending, runIO)@@ -74,8 +75,8 @@                     Just num -> num                     _ -> 1               must' = case must pack of-                Just num -> num-                _ -> 0+                Just num -> MtExact num+                _ -> MtDisabled           case skip pack of             Just True -> pending             _ -> do@@ -96,13 +97,13 @@                   if normalize'                     then pure normalizationRules                     else pure []-              rewritten <- rewrite' program rules' (RewriteContext program repeat' repeat' buildTerm must')+              rewritten <- rewrite' program rules' (RewriteContext program repeat' repeat' False buildTerm must')               result' <- parseProgramThrows (output pack)               unless (rewritten == result') $                 expectationFailure                   ( "Wrong rewritten program. Expected:\n"-                      ++ prettyProgram' result' SWEET+                      ++ prettyProgram' result' SWEET UNICODE                       ++ "\nGot:\n"-                      ++ prettyProgram' rewritten SWEET+                      ++ prettyProgram' rewritten SWEET UNICODE                   )       )
test/XMIRSpec.hs view
@@ -1,45 +1,123 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-}  -- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com -- SPDX-License-Identifier: MIT  module XMIRSpec where -import Control.Monad (forM_)+import Ast+import Control.Exception (bracket)+import Control.Monad (filterM, forM_, unless) import Data.Aeson+import Data.List (intercalate) import Data.Yaml qualified as Yaml import GHC.Generics (Generic)+import GHC.IO (unsafePerformIO) import Misc (allPathsIn)-import Parser (parseProgramThrows)+import Parser (parseExpressionThrows, parseProgramThrows)+import System.Directory (removeFile)+import System.Exit (ExitCode (ExitSuccess)) import System.FilePath (makeRelative)-import Test.Hspec (Spec, describe, it, runIO, shouldBe)-import XMIR (parseXMIRThrows, xmirToPhi)+import System.IO (hClose, hPutStr, openTempFile)+import System.Process (readProcessWithExitCode)+import Test.Hspec (Spec, anyException, describe, expectationFailure, it, pendingWith, runIO, shouldBe, shouldThrow)+import XMIR (defaultXmirContext, parseXMIRThrows, printXMIR, programToXMIR, xmirToPhi) -data XMIRPack = XMIRPack-  { xmir :: String,+data ParsePack = ParsePack+  { failure :: Maybe Bool,+    xmir :: String,     phi :: String   }   deriving (Generic, Show, FromJSON) -xmirPack :: FilePath -> IO XMIRPack-xmirPack = Yaml.decodeFileThrow+data PrintPack = PrintPack+  { phi :: String,+    xpaths :: [String]+  }+  deriving (Generic, Show, FromJSON) --- @todo #126:30min Introduce XMIR printing test. It's not possible anymore to compare XMIRs like strings---  because they contain random data, e.g. system time. We need to introduce some convenient---  test system for testing XML and use it here here.+parsePack :: FilePath -> IO ParsePack+parsePack = Yaml.decodeFileThrow++printPack :: FilePath -> IO PrintPack+printPack = Yaml.decodeFileThrow++-- Check if xmllint is available on the system+isXmllintAvailable :: Bool+isXmllintAvailable =+  let (exitCode, _, _) = unsafePerformIO (readProcessWithExitCode "xmllint" ["--version"] "")+   in (exitCode == ExitSuccess)+ spec :: Spec-spec =+spec = do   describe "XMIR parsing packs" $ do     let resources = "test-resources/xmir-parsing-packs"     packs <- runIO (allPathsIn resources)     forM_       packs       ( \pth -> it (makeRelative resources pth) $ do-          pack <- xmirPack pth-          xmir' <- do-            doc <- parseXMIRThrows (xmir pack)-            xmirToPhi doc-          phi' <- parseProgramThrows (phi pack)-          xmir' `shouldBe` phi'+          pack <- parsePack pth+          let ParsePack {phi = phi'} = pack+              xmir' = do+                doc <- parseXMIRThrows (xmir pack)+                xmirToPhi doc+          case failure pack of+            Just True -> xmir' `shouldThrow` anyException+            _ -> do+              xmir'' <- xmir'+              phi'' <- parseProgramThrows phi'+              xmir'' `shouldBe` phi''+      )++  describe "prohibit to convert to XMIR" $+    forM_+      [ "[[ ]]",+        "T",+        "[[ x -> ? ]]",+        "[[ ^ -> 5 ]]",+        "Q.x.y.z",+        "\"Hello\"",+        "Q",+        "$"+      ]+      ( \phi' -> it phi' $ do+          expr <- parseExpressionThrows phi'+          programToXMIR (Program expr) defaultXmirContext `shouldThrow` anyException+      )++  describe "XMIR printing packs" $ do+    let resources = "test-resources/xmir-printing-packs"+        available = isXmllintAvailable+    packs <- runIO (allPathsIn resources)+    forM_+      packs+      ( \pth ->+          it (makeRelative resources pth) $+            if not available+              then pendingWith "The 'xmllint' is not available"+              else do+                pack <- printPack pth+                let PrintPack {phi = phi', xpaths = xpaths'} = pack+                prog <- parseProgramThrows phi'+                xmir' <- programToXMIR prog defaultXmirContext+                let xml = printXMIR xmir'+                bracket+                  (openTempFile "." "xmirXXXXXX.tmp")+                  (\(fp, _) -> removeFile fp)+                  ( \(path, hTmp) -> do+                      hPutStr hTmp xml+                      hClose hTmp+                      failed <-+                        filterM+                          ( \xpath -> do+                              (code, _, _) <- readProcessWithExitCode "xmllint" ["--xpath", xpath, path] ""+                              pure (code /= ExitSuccess)+                          )+                          xpaths'+                      unless+                        (null failed)+                        (expectationFailure ("Failed xpaths:\n - " ++ intercalate "\n - " failed ++ "\nXMIR is:\n" ++ xml))+                  )       )
test/YamlSpec.hs view
@@ -3,14 +3,16 @@  module YamlSpec where +import Control.Exception (Exception (displayException), SomeException) import Control.Monad+import Data.List (isInfixOf) import Misc import System.FilePath-import Test.Hspec (Spec, describe, it, runIO, shouldReturn)+import Test.Hspec (Spec, describe, it, runIO, shouldReturn, shouldThrow) import Yaml (yamlRule)  spec :: Spec-spec =+spec = do   describe "parses yaml rule" $ do     let resources = "test-resources/yaml-packs"     packs <- runIO (allPathsIn resources)@@ -19,4 +21,19 @@       ( \pth -> it (makeRelative resources pth) $ do           _ <- yamlRule pth           pure () `shouldReturn` ()+      )++  describe "fails on yaml typos" $ do+    let resources = "test-resources/yaml-typos"+    packs <- runIO (allPathsIn resources)+    forM_+      packs+      ( \pth ->+          it (makeRelative resources pth) $+            shouldThrow+              (yamlRule pth)+              ( \e ->+                  let msg = displayException (e :: SomeException)+                   in "Unknown" `isInfixOf` msg || "Exactly one" `isInfixOf` msg+              )       )