{-# LANGUAGE OverloadedStrings #-} -- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com -- SPDX-License-Identifier: MIT module Functions (buildTerm) where import Ast import Builder import Control.Exception (throwIO) import Control.Monad (replicateM, when) import qualified Data.ByteString.Char8 as B import Data.Char (intToDigit) import Data.Functor import Data.Set (Set) import qualified Data.Set import GHC.IO (unsafePerformIO) import Matcher import Misc import Numeric (showHex) import Parser (parseAttributeThrows, parseNumberThrows) import Pretty import Random (randomString) import Regexp import System.Random (randomRIO) import Term import Text.Printf (printf) import qualified Yaml as Y buildTerm :: BuildTermFunc buildTerm "contextualize" = _contextualize buildTerm "scope" = _scope buildTerm "random-tau" = _randomTau buildTerm "dataize" = _dataize buildTerm "concat" = _concat buildTerm "sed" = _sed buildTerm "random-string" = _randomString buildTerm "size" = _size buildTerm "tau" = _tau buildTerm "string" = _string buildTerm "number" = _number buildTerm "sum" = _sum buildTerm func = _unsupported func argToBytes :: Y.ExtraArgument -> Subst -> Program -> IO Bytes argToBytes (Y.ArgBytes bytes) subst _ = buildBytesThrows bytes subst argToBytes (Y.ArgExpression expr) subst prog = do (TeBytes bts) <- _dataize [Y.ArgExpression expr] subst prog pure bts argToBytes arg _ _ = throwIO (userError (printf "Can't extract bytes from given argument: %s" (prettyExtraArg arg))) argToString :: Y.ExtraArgument -> Subst -> Program -> IO String argToString arg subst prog = argToBytes arg subst prog <&> btsToUnescapedStr argToNumber :: Y.ExtraArgument -> Subst -> Program -> IO Double argToNumber arg subst prog = argToBytes arg subst prog <&> either toDouble id . btsToNum _contextualize :: BuildTermMethod _contextualize [Y.ArgExpression expr, Y.ArgExpression context] subst prog = do (expr', _) <- buildExpressionThrows expr subst (context', _) <- buildExpressionThrows context subst pure (TeExpression (contextualize expr' context' prog)) _contextualize _ _ _ = throwIO (userError "Function contextualize() requires exactly 2 arguments as expression") _scope :: BuildTermMethod _scope [Y.ArgExpression expr] subst _ = do (_, scope) <- buildExpressionThrows expr subst pure (TeExpression scope) _scope _ _ _ = throwIO (userError "Function scope() requires exactly 1 argument as expression") _randomTau :: BuildTermMethod _randomTau args subst _ = do attrs <- argsToAttrs args tau <- randomTau attrs pure (TeAttribute (AtLabel tau)) where argsToAttrs :: [Y.ExtraArgument] -> IO [String] argsToAttrs [] = pure [] argsToAttrs (arg : rest) = case arg of Y.ArgExpression _ -> argsToAttrs rest Y.ArgAttribute attr -> do attr' <- buildAttributeThrows attr subst rest' <- argsToAttrs rest pure (prettyAttribute attr' : rest') Y.ArgBinding bd -> do bds <- buildBindingThrows bd subst rest' <- argsToAttrs rest pure (attrsFromBindings 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" if tau `elem` attrs then randomTau attrs else pure tau _dataize :: BuildTermMethod _dataize [Y.ArgBytes bytes] subst _ = do bts <- buildBytesThrows bytes subst pure (TeBytes bts) _dataize [Y.ArgExpression expr] subst _ = do (expr', _) <- buildExpressionThrows expr subst case expr' of DataObject _ bytes -> pure (TeBytes bytes) _ -> throwIO (userError "Only data objects and bytes are supported by 'dataize' function now") _dataize _ _ _ = throwIO (userError "Function dataize() requires exactly 1 argument as expression or bytes") _concat :: BuildTermMethod _concat args subst prog = do args' <- traverse (\arg -> argToString arg subst prog) args pure (TeExpression (DataString (strToBts (concat args')))) _sed :: BuildTermMethod _sed args subst prog = do when (length args < 2) (throwIO (userError "Function sed() requires at least two arguments")) args' <- traverse ( \arg -> do bts <- argToString arg subst prog pure (B.pack bts) ) args res <- sed (head args') (tail args') pure (TeExpression (DataString (strToBts (B.unpack res)))) where sed :: B.ByteString -> [B.ByteString] -> IO B.ByteString sed tgt [] = pure tgt sed tgt (ptn : ptns) = do (pat, rep, global) <- parse ptn regex <- compile pat next <- if global then replaceAll regex rep tgt else replaceFirst regex rep tgt sed next ptns parse :: B.ByteString -> IO (B.ByteString, B.ByteString, Bool) parse input = case B.stripPrefix "s/" input of Just body -> let (pat, rest) = nextUntilSlash body B.empty False (rep, flag) = nextUntilSlash rest B.empty True in case [pat, rep, flag] of [pat, rep, "g"] -> pure (pat, rep, True) [pat, rep, ""] -> pure (pat, rep, False) _ -> throwIO (userError "sed pattern must be in format s/pat/rep/[g]") _ -> throwIO (userError "sed pattern must start with s/") -- Cut part from given string until regular slash. nextUntilSlash :: B.ByteString -> B.ByteString -> Bool -> (B.ByteString, B.ByteString) nextUntilSlash input acc escape = case B.uncons input of Nothing -> (acc, B.empty) Just (h, rest) | h == '\\' -> case B.uncons rest of Just (h', rest') -> nextUntilSlash rest' ((if escape then acc else B.snoc acc '\\') `B.append` B.singleton h') escape Nothing -> (if escape then acc else B.snoc acc '\\', B.empty) | h == '/' -> (acc, rest) | otherwise -> nextUntilSlash rest (B.snoc acc h) escape _randomString :: BuildTermMethod _randomString [arg] subst prog = do pat <- argToString arg subst prog str <- randomString pat pure (TeExpression (DataString (strToBts str))) _randomString _ _ _ = throwIO (userError "Function random-string() requires exactly 1 dataizable argument") _size :: BuildTermMethod _size [Y.ArgBinding (BiMeta meta)] subst _ = do bds <- buildBindingThrows (BiMeta meta) subst pure (TeExpression (DataNumber (numToBts (fromIntegral (length bds))))) _size _ _ _ = throwIO (userError "Function size() requires exactly 1 meta binding") _tau :: BuildTermMethod _tau [Y.ArgExpression expr] subst prog = do TeBytes bts <- _dataize [Y.ArgExpression expr] subst prog attr <- parseAttributeThrows (btsToUnescapedStr bts) pure (TeAttribute attr) _tau _ _ _ = throwIO (userError "Function tau() requires exactly 1 argument as expression") _string :: BuildTermMethod _string [Y.ArgExpression expr] subst _ = do (expr', _) <- buildExpressionThrows expr subst str <- case expr' of DataNumber bts -> pure (DataString (strToBts (either show show (btsToNum bts)))) DataString bts -> pure (DataString bts) exp -> throwIO ( userError ( printf "Couldn't convert given expression to 'Φ̇.string' object, only 'Φ̇.number' or 'Φ̇.string' are allowed\n%s" (prettyExpression' exp) ) ) pure (TeExpression str) _string [Y.ArgAttribute attr] subst _ = do attr' <- buildAttributeThrows attr subst pure (TeExpression (DataString (strToBts (prettyAttribute attr')))) _string _ _ _ = throwIO (userError "Function string() requires exactly 1 argument as attribute or data expression (Φ̇.number or Φ̇.string)") _number :: BuildTermMethod _number [Y.ArgExpression expr] subst _ = do (expr', _) <- buildExpressionThrows expr subst case expr' of DataString bts -> do num <- parseNumberThrows (btsToUnescapedStr bts) pure (TeExpression num) _ -> throwIO (userError (printf "Function number() expects expression to be 'Φ̇.string', but got:\n%s" (prettyExpression' expr'))) _number _ _ _ = throwIO (userError "Function number() requires exactly 1 argument as 'Φ̇.string'") _sum :: BuildTermMethod _sum args subst prog = do nums <- traverse (\arg -> argToNumber arg subst prog) args pure (TeExpression (DataNumber (numToBts (sum nums)))) _unsupported :: BuildTermFunc _unsupported func _ _ _ = throwIO (userError (printf "Function %s() is not supported or does not exist" func))