packages feed

phino-0.0.152: src/CLI/Runners.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com
-- SPDX-License-Identifier: MIT

module CLI.Runners where

import AST
import CLI.Helpers
import CLI.Types
import CLI.Validators
import Condition (parseConditionThrows)
import Control.Concurrent (rtsSupportsBoundThreads, setNumCapabilities)
import Control.Exception
import Control.Monad (unless, when)
import Data.Foldable (traverse_)
import Data.IORef (newIORef)
import Data.List (intercalate)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import Data.Maybe (fromJust, isJust, isNothing, listToMaybe)
import qualified Data.Text as T
import Dataize
import Deps (Judgment (..), dontSaveMade)
import Emit (emitted)
import Encoding
import Engine (Engine (..), building, current, stepOf)
import Evaluate (evaluation, fired)
import Files (overwrite)
import qualified Filter as F
import LaTeX (explainContextualizeRules, explainDataizeRules, explainMorphRules, explainRules)
import Logger
import Margin (defaultMargin)
import Merge (merge)
import Metas (Metas (..))
import Morph
import Parser (parseExpressionThrows)
import qualified Printer as P
import qualified Random as R
import Rewriter
import Rule (RuleContext (..), matchExpressionWithRule)
import Slots (anonymous)
import System.Directory (canonicalizePath, doesFileExist, getModificationTime)
import System.Exit (exitSuccess)
import System.FilePath (takeDirectory, (</>))
import System.Random (mkStdGen, setStdGen)
import Tau (seedTaus)
import Text.Printf (printf)
import XMIR
import qualified Yaml as Y

runRewrite :: OptsRewrite -> IO ()
runRewrite OptsRewrite{..} = do
  validateOpts
  checkUpdate
  excluded <- validatedDispatches "hide" _hide
  included <- validatedDispatches "show" _show
  [loc] <- validatedDispatches "locator" [_locator]
  [foc] <- validatedDispatches "focus" [_focus]
  validateNoOverlap "show" included "hide" excluded
  setStdGen (mkStdGen _seed)
  rules <- getRules _normalize _shuffle _rules
  linked <- engine
  validateBreakpoint _breakpoint rules
  input <- readInput _inputFile
  (expr, atoms) <- parseInputWithAtoms input _inputFormat
  validateXmirTopLevel _outputFormat expr
  seedTaus expr
  logDebug (printf "Amount of rewriting cycles across all the rules: %d, per rule: %d" _maxCycles _maxDepth)
  let listing = case (rules, _inputFormat) of
        ([], PHI) -> (\_ -> escapeXMLText input)
        (_, _) -> (\rewritten -> escapeXMLText (P.printExpression' rewritten (_sugarType, UNICODE, _flat, _margin)))
      xmirCtx = XmirContext _omitListing _omitComments _hideRho listing atoms
      printCtx = toPrintCtx xmirCtx foc
      exclude = (`F.exclude` excluded)
      include = (`F.include` included)
  save <- saveStepFunc _stepsDir printCtx included excluded
  let steps = map (stepOf linked) rules
  (rewrittens, exceeded) <- rewrite expr steps (RewriteContext loc _maxDepth _maxCycles _depthSensitive Nothing (building linked) linked._normal (every steps) _must _breakpoint save dontSaveMade)
  rewrittens' <- include (if _sequence then NE.toList rewrittens else [NE.last rewrittens]) >>= exclude
  logDebug (printf "Printing rewritten 𝜑-expression as %s" (show _outputFormat))
  exprs <- printRewrittens printCtx (rewrittens', exceeded)
  output _targetFile exprs
  where
    validateOpts :: IO ()
    validateOpts = do
      when (_inPlace && isNothing _inputFile) (invalidCLIArguments "The option --in-place requires an input file")
      when (_inPlace && isJust _targetFile) (invalidCLIArguments "The options --in-place and --target cannot be used together")
      when (_inPlace && _outputFormat /= _inputFormat) (invalidCLIArguments "The option --in-place requires the output format to match the input format")
      when (_inPlace && _sequence) (invalidCLIArguments "The options --in-place and --sequence cannot be used together, since the file must keep one program")
      focusedExpr <- parseExpressionThrows _focus
      when (_inPlace && focusedExpr /= ExRoot) (invalidCLIArguments "The options --in-place and --focus cannot be used together, since the file must keep the whole program")
      when (_inPlace && not (null _show)) (invalidCLIArguments "The options --in-place and --show cannot be used together, since the file must keep the whole program")
      when (_inPlace && not (null _hide)) (invalidCLIArguments "The options --in-place and --hide cannot be used together, since the file must keep the whole program")
      when (_inPlace && _hideRho) (invalidCLIArguments "The options --in-place and --hide-rho cannot be used together, since the file must keep the whole program")
      when (_update && _inPlace) (invalidCLIArguments "The options --update and --in-place cannot be used together")
      when (_update && isNothing _targetFile) (invalidCLIArguments "The option --update requires --target")
      when (_update && isNothing _inputFile) (invalidCLIArguments "The option --update requires an input file")
      when (length _show > 1) (invalidCLIArguments "The option --show can be used only once")
      validateLatexOptions
        _outputFormat
        [(_nonumber, "nonumber"), (_compress, "compress")]
        [(_expression, "expression"), (_label, "label"), (_meetPrefix, "meet-prefix")]
        [(_meetPopularity, "meet-popularity"), (_meetLength, "meet-length")]
      validateMust' _must
      validateXmirOptions _outputFormat [(_omitListing, "omit-listing"), (_omitComments, "omit-comments")] _focus
    checkUpdate :: IO ()
    checkUpdate = case (_update, _inputFile, _targetFile) of
      (True, Just src, Just tgt) -> do
        exists <- doesFileExist tgt
        when exists $ do
          newer <- (>) <$> getModificationTime tgt <*> getModificationTime src
          when newer $ do
            logDebug (printf "Target '%s' is newer than source '%s', skipping rewriting (--update)" tgt src)
            exitSuccess
      _ -> pure ()
    validateBreakpoint :: Maybe String -> [Y.Rule] -> IO ()
    validateBreakpoint Nothing _ = pure ()
    validateBreakpoint (Just rule) rules =
      let names = map (.name) rules
       in unless
            (rule `elem` names)
            (invalidCLIArguments (printf "The rule '%s' provided in '--breakpoint' option is absent across given rewriting rules: %s" rule (intercalate ", " names)))
    output :: Maybe FilePath -> String -> IO ()
    output target expr = case (_inPlace, target, _inputFile) of
      (True, _, Just file) -> do
        logDebug (printf "The option '--in-place' is specified, writing back to '%s'..." file)
        overwrite file (expr ++ "\n")
        logDebug (printf "The file '%s' was modified in-place" file)
      (True, _, Nothing) ->
        error "The option --in-place requires an input file"
      (False, Just file, _) -> do
        logDebug (printf "The option '--target' is specified, printing to '%s'..." file)
        overwrite file (expr ++ "\n")
        logDebug (printf "The command result was saved in '%s'" file)
      (False, Nothing, _) -> do
        logDebug "The option '--target' is not specified, printing to console..."
        putStrLn expr
    toPrintCtx :: XmirContext -> Expression -> PrintContext
    toPrintCtx xmirCtx focus =
      PrintCtx
        _sugarType
        _hideRho
        Nothing
        False
        _flat
        _margin
        xmirCtx
        _nonumber
        _compress
        _canonize
        _sequence
        _headers
        (justMeetPopularity _meetPopularity)
        (justMeetLength _meetLength)
        focus
        _expression
        _label
        _meetPrefix
        _outputFormat

runDataize :: OptsDataize -> IO ()
runDataize OptsDataize{..} = do
  validateOpts
  deadline <- timed _maxSeconds
  lambdas <- lambdasOf _symbolic
  excluded <- validatedDispatches "hide" _hide
  included <- validatedDispatches "show" _show
  [loc] <- validatedDispatches "locator" [_locator]
  [foc] <- validatedDispatches "focus" [_focus]
  validateNoOverlap "show" included "hide" excluded
  input <- readInput _inputFile
  (expr, atoms) <- parseInputWithAtoms input _inputFormat
  setStdGen (mkStdGen _seed)
  seedTaus expr
  let printCtx = toPrintCtx atoms foc
      exclude = (`F.exclude` excluded)
      include = (`F.include` included)
  save <- saveStepFunc _stepsDir printCtx included excluded
  tally <- tallied _maxFirings
  minted <- newIORef 0
  memo <- memoized _acyclic
  linked <- engine
  (outcome, chain, _) <-
    withEvalFunc
      _protocol
      printCtx
      ( \record -> do
          let ctx = ReduceContext loc loc Nothing _maxDepth _maxCycles (Steps _maxSteps 0) tally minted deadline memo 1 (Just (1, Dataization, loc)) _depthSensitive _shuffle _partial False 1 _acyclic Dataization [] Map.empty lambdas [] (building linked) reduction evaluation fired save record linked
          (universe, aiming) <- aimed printCtx Dataization _inside expr ctx
          started universe aiming
          dataize universe emptyState aiming
      )
  when _sequence (include chain >>= exclude >>= \shown -> printRewrittens printCtx (shown, False) >>= putStrLn)
  unless _quiet (printOutcome printCtx (\residue -> F.include' residue included >>= (`F.exclude'` excluded)) outcome >>= putStrLn)
  where
    printOutcome :: PrintContext -> (Expression -> IO Expression) -> Outcome -> IO String
    printOutcome _ _ (Dataized bytes) = pure (P.printBytes bytes)
    printOutcome ctx narrowed (Residual residue) = do
      logDebug "Dataization got stuck on a λ function that cannot fire, printing the residual program (--partial)"
      answer <- narrowed residue
      validateXmirTopLevel _outputFormat answer
      printAnswer ctx answer
    validateOpts :: IO ()
    validateOpts = do
      validateLatexOptions
        _outputFormat
        [(_nonumber, "nonumber"), (_compress, "compress")]
        [(_expression, "expression"), (_label, "label"), (_meetPrefix, "meet-prefix")]
        [(_meetPopularity, "meet-popularity"), (_meetLength, "meet-length")]
      validateXmirOptions _outputFormat [(_omitListing, "omit-listing"), (_omitComments, "omit-comments")] _focus
      when (length _show > 1) (invalidCLIArguments "The option --show can be used only once")
      when (isJust _abridged && isNothing _protocol) (invalidCLIArguments "The option --abridged requires --protocol, since only the protocol is abridged")
      when (_abridgedData && isNothing _abridged) (invalidCLIArguments "The option --abridged-data requires --abridged, since only an abridged protocol cuts its data")
      when
        (isJust _inside && _locator /= "Q")
        (invalidCLIArguments "The options --inside and --locator cannot be used together, since --inside aims the run at the binding it mints")
    toPrintCtx :: Atoms -> Expression -> PrintContext
    toPrintCtx atoms focus =
      PrintCtx
        _sugarType
        _hideRho
        _abridged
        _abridgedData
        _flat
        _margin
        (XmirContext _omitListing _omitComments _hideRho listing atoms)
        _nonumber
        _compress
        _canonize
        _sequence
        _headers
        (justMeetPopularity _meetPopularity)
        (justMeetLength _meetLength)
        focus
        _expression
        _label
        _meetPrefix
        _outputFormat
    listing :: Expression -> String
    listing e = escapeXMLText (P.printExpression' e (_sugarType, UNICODE, _flat, _margin))

runMorph :: OptsMorph -> IO ()
runMorph OptsMorph{..} = do
  validateOpts
  deadline <- timed _maxSeconds
  when rtsSupportsBoundThreads (setNumCapabilities _jobs)
  lambdas <- lambdasOf _symbolic
  excluded <- validatedDispatches "hide" _hide
  included <- validatedDispatches "show" _show
  [loc] <- validatedDispatches "locator" [_locator]
  [foc] <- validatedDispatches "focus" [_focus]
  validateNoOverlap "show" included "hide" excluded
  input <- readInput _inputFile
  (expr, atoms) <- parseInputWithAtoms input _inputFormat
  setStdGen (mkStdGen _seed)
  seedTaus expr
  let printCtx = toPrintCtx atoms foc
      exclude = (`F.exclude` excluded)
      include = (`F.include` included)
  save <- saveStepFunc _stepsDir printCtx included excluded
  tally <- tallied _maxFirings
  minted <- newIORef 0
  memo <- memoized _acyclic
  linked <- engine
  (morphed, chain, _) <-
    withEvalFunc
      _protocol
      printCtx
      ( \record -> do
          let ctx = ReduceContext loc loc Nothing _maxDepth _maxCycles (Steps _maxSteps 0) tally minted deadline memo 1 (Just (1, Morphing, loc)) _depthSensitive _shuffle _partial _deep _jobs _acyclic Morphing [] Map.empty lambdas [] (building linked) reduction evaluation fired save record linked
          (universe, aiming) <- aimed printCtx Morphing _inside expr ctx
          started universe aiming
          morph universe emptyState aiming
      )
  printed <-
    if _quiet
      then pure Nothing
      else do
        answer <- F.include' (if foc == ExRoot then morphed else maybe morphed fst (lastMaybe chain)) included >>= (`F.exclude'` excluded)
        validateXmirTopLevel _outputFormat answer
        Just <$> printAnswer printCtx answer
  when _sequence (include chain >>= exclude >>= \shown -> printRewrittens printCtx (shown, False) >>= putStrLn)
  mapM_ putStrLn printed
  where
    lastMaybe :: [a] -> Maybe a
    lastMaybe [] = Nothing
    lastMaybe items = Just (last items)
    validateOpts :: IO ()
    validateOpts = do
      validateLatexOptions
        _outputFormat
        [(_nonumber, "nonumber"), (_compress, "compress")]
        [(_expression, "expression"), (_label, "label"), (_meetPrefix, "meet-prefix")]
        [(_meetPopularity, "meet-popularity"), (_meetLength, "meet-length")]
      validateXmirOptions _outputFormat [(_omitListing, "omit-listing"), (_omitComments, "omit-comments")] _focus
      when (length _show > 1) (invalidCLIArguments "The option --show can be used only once")
      when (isJust _abridged && isNothing _protocol) (invalidCLIArguments "The option --abridged requires --protocol, since only the protocol is abridged")
      when (_abridgedData && isNothing _abridged) (invalidCLIArguments "The option --abridged-data requires --abridged, since only an abridged protocol cuts its data")
      when (_jobs > 1 && not _deep) (invalidCLIArguments "The option --jobs requires --deep, since only the deep walk runs on several workers")
      when
        (isJust _inside && _locator /= "Q")
        (invalidCLIArguments "The options --inside and --locator cannot be used together, since --inside aims the run at the binding it mints")
    toPrintCtx :: Atoms -> Expression -> PrintContext
    toPrintCtx atoms focus =
      PrintCtx
        _sugarType
        _hideRho
        _abridged
        _abridgedData
        _flat
        _margin
        (XmirContext _omitListing _omitComments _hideRho listing atoms)
        _nonumber
        _compress
        _canonize
        _sequence
        _headers
        (justMeetPopularity _meetPopularity)
        (justMeetLength _meetLength)
        focus
        _expression
        _label
        _meetPrefix
        _outputFormat
    listing :: Expression -> String
    listing e = escapeXMLText (P.printExpression' e (_sugarType, UNICODE, _flat, _margin))

runExplain :: OptsExplain -> IO ()
runExplain OptsExplain{..} = do
  setStdGen (mkStdGen _seed)
  validateOpts
  explained >>= printOut _targetFile
  where
    explained :: IO String
    explained
      | _morph = explainMorphRules <$> shuffled Y.morphingRules
      | _dataize = explainDataizeRules <$> shuffled Y.dataizationRules
      | _contextualize = explainContextualizeRules <$> shuffled Y.contextualizationRules
      | otherwise = explainRules <$> getRules _normalize _shuffle _rules
    shuffled :: [a] -> IO [a]
    shuffled xs
      | _shuffle = R.shuffle xs
      | otherwise = pure xs
    validateOpts :: IO ()
    validateOpts = do
      let selected = length (filter id [_morph, _dataize, _contextualize])
      when (selected == 0 && null _rules && not _normalize) (invalidCLIArguments "Either --rule, --normalize, --morph, --dataize or --contextualize must be specified")
      when (selected > 1) (invalidCLIArguments "Only one of --morph, --dataize or --contextualize can be specified")
      when (selected == 1 && not (null _rules)) (invalidCLIArguments "The --rule option cannot be used together with --morph, --dataize or --contextualize")
      when (selected == 1 && _normalize) (invalidCLIArguments "The --normalize option cannot be used together with --morph, --dataize or --contextualize")

runMerge :: OptsMerge -> IO ()
runMerge OptsMerge{..} = do
  validateOpts
  inputs' <- traverse (readInput . Just) _inputs
  setStdGen (mkStdGen _seed)
  (exprs, atoms) <- unzip <$> traverse (`parseInputWithAtoms` _inputFormat) inputs'
  expr <- merge exprs
  validateXmirTopLevel _outputFormat expr
  let listing = const (escapeXMLText (P.printExpression' expr (_sugarType, UNICODE, _flat, _margin)))
      xmirCtx = XmirContext _omitListing _omitComments False listing (Map.unions atoms)
      printCtx = toPrintCtx xmirCtx
  expr' <- printInFormat printCtx expr
  printOut _targetFile expr'
  where
    validateOpts :: IO ()
    validateOpts = do
      when (null _inputs) (throwIO (InvalidCLIArguments "At least one input file must be specified for 'merge' command"))
      validateXmirOptions _outputFormat [(_omitListing, "omit-listing"), (_omitComments, "omit-comments")] "Q"
    toPrintCtx :: XmirContext -> PrintContext
    toPrintCtx xmirCtx =
      PrintCtx
        _sugarType
        False
        Nothing
        False
        _flat
        _margin
        xmirCtx
        False
        False
        False
        False
        False
        (justMeetPopularity Nothing)
        (justMeetLength Nothing)
        ExRoot
        Nothing
        Nothing
        Nothing
        _outputFormat

runMatch :: OptsMatch -> IO ()
runMatch OptsMatch{..} = do
  when (isJust _when && isNothing _pattern) (invalidCLIArguments "The option --when requires --pattern, since there is nothing to check it against")
  setStdGen (mkStdGen _seed)
  input <- readInput _inputFile
  expr <- parseInput input PHI
  if isNothing _pattern
    then logDebug "The --pattern is not provided, no substitutions are built"
    else do
      ptn <- parseExpressionThrows (fromJust _pattern)
      condition <- traverse parseConditionThrows _when
      traverse_ (throwIO . AnonymousMetaInCondition . T.unpack) (anonymous condition)
      traverse_ (throwIO . UnboundMetaInCondition . T.unpack) (listToMaybe (unboundMetas ptn condition))
      linked <- engine
      substs <- matchExpressionWithRule expr (rule ptn condition) (RuleContext (building linked) Nothing linked._normal)
      if null substs
        then throwIO EmptySubstsOnMatch
        else putStrLn (P.printSubsts' substs (_sugarType, UNICODE, _flat, defaultMargin))
  where
    rule :: Expression -> Maybe Y.Condition -> Y.Rule
    rule ptn cnd = Y.Rule "custom" Nothing Nothing ptn ExRoot cnd Nothing Nothing
    unboundMetas :: Expression -> Maybe Y.Condition -> [T.Text]
    unboundMetas ptn condition = filter (`notElem` namedMetas ptn) (namedMetas condition)
    namedMetas :: (Metas a) => a -> [T.Text]
    namedMetas term = filter ((> 1) . T.length) (metas term)

runCompile :: OptsCompile -> IO ()
runCompile OptsCompile{..} = do
  custom <- getRules False False _rules
  source <- either (throwIO . CouldNotCompile) pure (emitted Y.normalizationRules custom Y.contextualizationRules Y.morphingRules Y.dataizationRules current)
  overwrite _targetFile source
  logInfo (printf "The rules were compiled into '%s'" _targetFile)
  root <- projectRootOf (takeDirectory _targetFile)
  case root of
    Nothing -> putStrLn "package phino\n  flags: +compiled"
    Just dir -> do
      let localFile = dir </> "cabal.project.local"
      exists <- doesFileExist localFile
      if exists
        then putStrLn (printf "The file '%s' exists, so add these lines to it to link the compiled rules in:\npackage phino\n  flags: +compiled" localFile)
        else do
          overwrite localFile "package phino\n  flags: +compiled\n"
          logInfo (printf "The file '%s' was written, so the next build links the compiled rules in" localFile)
  where
    projectRootOf :: FilePath -> IO (Maybe FilePath)
    projectRootOf dir = do
      full <- canonicalizePath dir
      found <- doesFileExist (full </> "phino.cabal")
      let parent = takeDirectory full
      if found
        then pure (Just full)
        else if parent == full then pure Nothing else projectRootOf parent