packages feed

axel-0.0.13: src/Axel/Sourcemap.hs

{-# LANGUAGE TemplateHaskell #-}

module Axel.Sourcemap where

import Axel.Prelude

import Axel.Eff.Lens (assign, modifying)
import Axel.Eff.Loop (breakLoop)
import qualified Axel.Eff.Loop as Effs (runLoop)
import qualified Axel.Parse.AST as Parse
  ( Expression(LiteralInt, LiteralString, SExpression, Symbol)
  , quoteExpression
  )
import Axel.Utils.List (unsafeHead)
import Axel.Utils.Text (Renderer)
import Axel.Utils.Tuple (Annotated, annotate, annotation, unannotate)

import Control.Lens ((|>))
import Control.Lens.Operators ((^.))
import Control.Lens.TH (makeFieldsNoPrefix, makeWrapped)
import Control.Monad (when)

import Data.Data (Data)
import Data.Foldable (for_)
import Data.Hashable (Hashable)
import Data.Map (Map)
import Data.MonoTraversable (oconcatMap, ofor_)
import qualified Data.Text as T

import qualified Effectful as Eff
import qualified Effectful.State.Static.Local as Eff

import GHC.Generics (Generic)

data Position =
  Position
    { _line :: Int
    , _column :: Int
    }
  deriving (Data, Eq, Generic, Show)

makeFieldsNoPrefix ''Position

instance Hashable Position

-- NOTE We're using `String` instead of `FilePath` so that we don't have to rely
-- on `Axel.Prelude` or `-XOverloadedStrings` in user-facing code.
type SourcePosition = (String, Position)

renderSourcePosition :: Renderer SourcePosition
renderSourcePosition (filePath, position) =
  let filePath' =
        if filePath == ""
          then "<unknown>"
          else T.pack filePath
   in filePath' <>
      ":" <> showText (position ^. line) <> ":" <> showText (position ^. column)

type SourceMetadata = Maybe SourcePosition

newtype Output =
  Output [Annotated SourceMetadata Text]
  deriving (Eq, Show)

deriving instance Semigroup Output

deriving instance Monoid Output

makeWrapped ''Output

raw :: Output -> Text
raw (Output output) = oconcatMap unannotate output

unassociated :: Text -> Output
unassociated x = Output [annotate Nothing x]

type Expression = Parse.Expression SourceMetadata

isCompoundExpressionWrapperHead :: Expression -> Bool
isCompoundExpressionWrapperHead (Parse.Symbol _ "begin") = True
isCompoundExpressionWrapperHead _ = False

wrapCompoundExpressions :: [Expression] -> Expression
wrapCompoundExpressions stmts =
  Parse.SExpression Nothing (Parse.Symbol Nothing "begin" : stmts)

unwrapCompoundExpressions :: Expression -> [Expression]
unwrapCompoundExpressions (Parse.SExpression _ (Parse.Symbol _ "begin":stmts)) =
  stmts
unwrapCompoundExpressions _ =
  error "unwrapCompoundExpressions must be passed a top-level program!"

data Bracket
  = CurlyBraces
  | DoubleQuotes
  | Parentheses
  | SingleQuotes
  | SquareBrackets

openBracket :: Bracket -> Char
openBracket CurlyBraces = '{'
openBracket DoubleQuotes = '"'
openBracket Parentheses = '('
openBracket SingleQuotes = '\''
openBracket SquareBrackets = '['

closeBracket :: Bracket -> Char
closeBracket CurlyBraces = '}'
closeBracket DoubleQuotes = '"'
closeBracket Parentheses = ')'
closeBracket SingleQuotes = '\''
closeBracket SquareBrackets = ']'

tryRemoveBrackets :: Bracket -> Text -> Text
tryRemoveBrackets bracket text =
  if T.length text >= 2 &&
     T.head text == openBracket bracket && T.last text == closeBracket bracket
    then T.init $ T.tail text
    else text

surround :: Bracket -> Output -> Output
surround bracket x =
  let (startMetadata, endMetadata) =
        case x of
          Output [] -> (Nothing, Nothing)
          Output xs -> (unsafeHead xs ^. annotation, last xs ^. annotation)
   in Output [annotate startMetadata (T.singleton $ openBracket bracket)] <>
      x <> Output [annotate endMetadata (T.singleton $ closeBracket bracket)]

data Delimiter
  = Commas
  | Newlines
  | Pipes
  | Semicolons
  | Spaces

delimit :: Delimiter -> [Output] -> Output
delimit delimiter =
  Output .
  tryInit . -- Remove unneeded final delimiter
  concatMap
    (\(Output x) ->
       let metadata =
             case x of
               [] -> Nothing
               (x':_) -> x' ^. annotation
        in x |> annotate metadata (renderDelimiter delimiter))
  where
    tryInit :: [a] -> [a]
    tryInit [] = []
    tryInit xs = init xs
    renderDelimiter :: Delimiter -> Text
    renderDelimiter Commas = ","
    renderDelimiter Newlines = "\n"
    renderDelimiter Pipes = "|"
    renderDelimiter Semicolons = ";"
    renderDelimiter Spaces = " "

renderBlock :: [Output] -> Output
renderBlock = surround CurlyBraces . delimit Semicolons

-- | Given a position in some transpiled output, find the corresponding
--   metadata in the original source.
--   Behavior is undefined if `column transPos == 0`.
--
--   TODO Make algorithm functional (assuming this can be cleanly done so).
findOriginalPosition ::
     forall ann. [Annotated ann Text] -> Position -> Maybe ann
findOriginalPosition output transPos =
  Eff.runPureEff $
  Eff.evalState (Position {_line = 1, _column = 0}) $
  Effs.runLoop $ do
    for_ output $ \chunk ->
      ofor_ (unannotate chunk) $ \char -> do
        if char == '\n'
          then do
            assign @Position column 0
            modifying @Position line succ
          else modifying @Position column succ
        Eff.get >>= \newSrcPos ->
          when (newSrcPos == transPos) $ breakLoop (Just $ chunk ^. annotation)
    pure Nothing

-- TODO Derive this with Template Haskell (it's currently very brittle)
quoteString :: String -> Expression
quoteString = Parse.LiteralString Nothing

-- TODO Derive this with Template Haskell (it's currently very brittle)
quotePosition :: Position -> Expression
quotePosition sourcePosition =
  Parse.SExpression
    Nothing
    [ Parse.Symbol Nothing "AxelRuntime_Sourcemap.Position"
    , Parse.LiteralInt Nothing (sourcePosition ^. line)
    , Parse.LiteralInt Nothing (sourcePosition ^. column)
    ]

-- TODO Derive this with Template Haskell (it's currently very brittle)
quote2Tuple :: (a -> Expression, b -> Expression) -> (a, b) -> Expression
quote2Tuple (quoterA, quoterB) (a, b) =
  Parse.SExpression Nothing [Parse.Symbol Nothing ",", quoterA a, quoterB b]

-- TODO Derive this with Template Haskell (it's currently very brittle)
quoteMaybe :: (a -> Expression) -> Maybe a -> Expression
quoteMaybe _ Nothing = Parse.Symbol Nothing "AxelRuntime_GHCPrelude.Nothing"
quoteMaybe quoter (Just x) =
  Parse.SExpression
    Nothing
    [Parse.Symbol Nothing "AxelRuntime_GHCPrelude.Just", quoter x]

quoteSourceMetadata :: Maybe SourcePosition -> Expression
quoteSourceMetadata = quoteMaybe $ quote2Tuple (quoteString, quotePosition)

quoteSMExpression :: Expression -> Expression
quoteSMExpression = Parse.quoteExpression quoteSourceMetadata

-- | Keys are the module file paths, and values are (the module name, the transpiled output).
type ModuleInfo = Map FilePath (Text, Maybe Output)