packages feed

phino-0.0.0.22: src/Rewriter.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE RecordWildCards #-}

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

module Rewriter (rewrite, rewrite', RewriteContext (..)) where

import Ast
import Builder
import qualified Condition as C
import Data.Foldable (foldlM)
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust)
import Debug.Trace (trace)
import Logger (logDebug)
import Matcher (MetaValue (MvAttribute, MvBindings, MvBytes, MvExpression), Subst (Subst), combine, combineMany, defaultScope, matchProgram, substEmpty, substSingle)
import Misc (ensuredFile)
import Parser (parseProgram, parseProgramThrows)
import Pretty
import Replacer (replaceProgram, replaceProgramThrows)
import Term
import Text.Printf
import Yaml (ExtraArgument (..))
import qualified Yaml as Y

data RewriteContext = RewriteContext
  { _program :: Program,
    _maxDepth :: Integer,
    _buildTerm :: BuildTermFunc
  }

-- Build pattern and result expression and replace patterns to results in given program
buildAndReplace :: Program -> Expression -> Expression -> [Subst] -> IO Program
buildAndReplace program ptn res substs = do
  ptns <- buildExpressions ptn substs
  repls <- buildExpressions res substs
  let repls' = map fst repls
      ptns' = map fst ptns
  replaceProgramThrows program ptns' repls'

-- Extend list of given substitutions with extra substitutions from 'where' yaml rule section
extraSubstitutions :: Maybe [Y.Extra] -> [Subst] -> RewriteContext -> IO [Subst]
extraSubstitutions extras substs RewriteContext {..} = case extras of
  Nothing -> pure substs
  Just extras' -> do
    res <-
      sequence
        [ foldlM
            ( \(Just subst') extra -> do
                let maybeName = case Y.meta extra of
                      ArgExpression (ExMeta name) -> Just name
                      ArgAttribute (AtMeta name) -> Just name
                      ArgBinding (BiMeta name) -> Just name
                      ArgBytes (BtMeta name) -> Just name
                      _ -> Nothing
                    func = Y.function extra
                    args = Y.args extra
                term <- _buildTerm func args subst' _program
                meta <- case term of
                  TeExpression expr -> do
                    logDebug (printf "Function %s() returned expression:\n%s" func (prettyExpression' expr))
                    pure (MvExpression expr defaultScope)
                  TeAttribute attr -> do
                    logDebug (printf "Function %s() returned attribute:\n%s" func (prettyAttribute attr))
                    pure (MvAttribute attr)
                  TeBytes bytes -> do
                    logDebug (printf "Function %s() returned bytes: %s" func (prettyBytes bytes))
                    pure (MvBytes bytes)
                case maybeName of
                  Just name -> pure (combine (substSingle name meta) subst')
                  _ -> pure Nothing
            )
            (Just subst)
            extras'
          | subst <- substs
        ]
    pure (catMaybes res)

rewrite :: Program -> [Y.Rule] -> RewriteContext -> IO Program
rewrite program [] _ = pure program
rewrite program (rule : rest) ctx = do
  let ptn = Y.pattern rule
      res = Y.result rule
      condition = Y.when rule
  maybeMatched <- C.matchProgramWithCondition ptn condition program
  prog <- case maybeMatched of
    Nothing -> pure program
    Just matched -> do
      let ruleName = fromMaybe "unknown" (Y.name rule)
      logDebug (printf "Rule '%s' has been matched, applying..." ruleName)
      substs <- extraSubstitutions (Y.where_ rule) matched ctx
      prog' <- buildAndReplace program ptn res substs
      if program == prog'
        then logDebug (printf "Applied '%s', no changes made" ruleName)
        else
          logDebug
            ( printf
                "Applied '%s' (%d nodes -> %d nodes):\n%s"
                ruleName
                (countNodes program)
                (countNodes prog')
                (prettyProgram' prog' SWEET)
            )
      pure prog'
  rewrite prog rest ctx

-- @todo #169:30min Memorize previous rewritten programs. Right now in order not to
--  get an infinite recursion during rewriting we just count have many times we apply
--  rewriting rules. If we reach given amount - we just stop. It's not idiomatic and may
--  not work on big programs. We need to introduce some mechanism which would memorize
--  all rewritten program on each step and if on some step we get the program that have already
--  been memorized - we fail because we got into infinite recursion. Ofc we should keep counting
--  rewriting cycles if program just only grows on each rewriting.
rewrite' :: Program -> [Y.Rule] -> RewriteContext -> IO Program
rewrite' prog rules ctx = _rewrite prog 0
  where
    _rewrite :: Program -> Integer -> IO Program
    _rewrite prog count = do
      let depth = _maxDepth ctx
      logDebug (printf "Starting rewriting cycle %d out of %d" count depth)
      if count == depth
        then do
          logDebug (printf "Max amount of rewriting cycles has been reached, rewriting is stopped")
          pure prog
        else do
          rewritten <- rewrite prog rules ctx
          if rewritten == prog
            then do
              logDebug "No rule matched, rewriting is stopped"
              pure rewritten
            else _rewrite rewritten (count + 1)