packages feed

aihc-parser-6.0.0.0: src/Aihc/Parser.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}

-- |
-- Module      : Aihc.Parser
-- Description : Haskell parser for the AIHC compiler
-- License     : Unlicense
--
-- This module provides parsing functions for Haskell source code.
-- The main entry point is 'parseModule' for parsing complete Haskell modules.
-- Additional functions are provided for parsing individual expressions,
-- patterns, and types.
module Aihc.Parser
  ( -- * Parsing modules
    parseModule,
    parseModuleWithTrace,
    TraceEntry (..),
    tokenKindWithoutSpacing,

    -- * Configuration
    ParserConfig (..),
    defaultConfig,

    -- * Parse results
    ParseResult (..),
    formatParseErrors,

    -- * Parsing expressions, patterns, types, and declarations
    parseExpr,
    parseSignatureType,
    parseType,
    parsePattern,
    parseDecl,
  )
where

import Aihc.Parser.Internal.Common (TokParser, drainParseErrors, eofTok)
import Aihc.Parser.Internal.Decl (declParser)
import Aihc.Parser.Internal.Errors (parseErrorBundleToSpannedText, parseErrorsToSpannedText)
import Aihc.Parser.Internal.Expr (exprParser)
import Aihc.Parser.Internal.Module (moduleParser)
import Aihc.Parser.Internal.Pattern (patternParser)
import Aihc.Parser.Internal.Type (typeParser, typeSignatureParser)
import Aihc.Parser.Lex (LexTokenKind (..))
import Aihc.Parser.Pretty ()
import Aihc.Parser.Syntax (Decl, Expr, Extension, Module (..), Pattern, SourceSpan, Type, applyImpliedExtensions, sourceSpanEndCol, sourceSpanSourceName, sourceSpanStartCol, sourceSpanStartLine, sourceSpanStartOffset, pattern SourceSpan)
import Aihc.Parser.Types
import Data.ByteString qualified as BS
import Data.List qualified as List
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Word (Word8)
import Prettyprinter (Doc, colon, defaultLayoutOptions, layoutPretty, pretty, vcat)
import Prettyprinter.Render.String (renderString)
import Text.Megaparsec qualified as MP

-- $setup
-- >>> :set -XOverloadedStrings
-- >>> import Aihc.Parser
-- >>> import Aihc.Parser.Syntax (moduleName)
-- >>> import Aihc.Parser.Shorthand (Shorthand(..))

-- | Default parser configuration.
--
-- * 'parserSourceName' is set to @\"\<input\>\"@
-- * 'parserExtensions' is empty (no extensions enabled by default)
--
-- >>> parserSourceName defaultConfig
-- "<input>"
--
-- >>> parserExtensions defaultConfig
-- []
defaultConfig :: ParserConfig
defaultConfig =
  ParserConfig
    { parserSourceName = "<input>",
      parserExtensions = []
    }

-- | Parse a Haskell expression.
--
-- >>> shorthand $ parseExpr defaultConfig "1 + 2"
-- ParseOk (EInfix (EInt 1 TInteger) "+" (EInt 2 TInteger))
--
-- >>> shorthand $ parseExpr defaultConfig "\\x -> x + 1"
-- ParseOk (ELambdaPats [PVar "x"] (EInfix (EVar "x") "+" (EInt 1 TInteger)))
--
-- Parse errors are returned as 'ParseErr':
--
-- >>> case parseExpr defaultConfig "1 +" of { ParseErr _ -> "error"; ParseOk _ -> "ok" }
-- "error"
parseExpr :: ParserConfig -> Text -> ParseResult Expr
parseExpr = runEntry mkTokStream (exprParser <* eofTok)

-- | Parse a Haskell pattern.
--
-- >>> shorthand $ parsePattern defaultConfig "(x, y)"
-- ParseOk (PTuple [PVar "x", PVar "y"])
--
-- >>> shorthand $ parsePattern defaultConfig "Just x"
-- ParseOk (PCon "Just" [PVar "x"])
parsePattern :: ParserConfig -> Text -> ParseResult Pattern
parsePattern = runEntry mkTokStream (patternParser <* eofTok)

-- | Parse a Haskell signature type.
--
-- This is the context used by top-level type signatures. Unlike 'parseType',
-- it rejects an unparenthesized outer kind signature:
--
-- >>> case parseSignatureType defaultConfig "_ :: _" of { ParseErr _ -> "error"; ParseOk _ -> "ok" }
-- "error"
parseSignatureType :: ParserConfig -> Text -> ParseResult Type
parseSignatureType = runEntry mkTokStream (typeSignatureParser <* eofTok)

-- | Parse a Haskell type in the general declaration RHS context.
--
-- >>> shorthand $ parseType defaultConfig "Int -> Bool"
-- ParseOk (TFun (TCon "Int") (TCon "Bool"))
--
-- >>> shorthand $ parseType defaultConfig "Maybe a"
-- ParseOk (TApp (TCon "Maybe") (TVar "a"))
--
-- >>> shorthand $ parseType defaultConfig "_ :: _"
-- ParseOk (TKindSig (TWildcard) (TWildcard))
parseType :: ParserConfig -> Text -> ParseResult Type
parseType = runEntry mkTokStream (typeParser <* eofTok)

-- | Parse a single Haskell declaration.
--
-- >>> shorthand $ parseDecl defaultConfig "f x = x + 1"
-- ParseOk (DeclValue (FunctionBind "f" [Match {MatchHeadPrefix, [PVar "x"], EInfix (EVar "x") "+" (EInt 1 TInteger)}]))
parseDecl :: ParserConfig -> Text -> ParseResult Decl
parseDecl = runEntry mkTokStream (declParser <* eofTok)

-- | Parse a complete Haskell module.
--
-- Returns any recovered parse errors alongside a (possibly partial) 'Module'.
-- When individual declarations fail to parse, the parser recovers and continues,
-- returning the error and the successfully parsed declarations.
--
-- >>> shorthand $ snd $ parseModule defaultConfig "module Main where\nmain = putStrLn \"Hello\""
-- Module {ModuleHead {"Main"}, [DeclValue (PatternBind (PVar "main") (EApp (EVar "putStrLn") (EString "Hello")))]}
--
-- Modules without a header are also supported:
--
-- >>> case parseModule defaultConfig "x = 1" of { (_, m) -> moduleName m }
-- Nothing
parseModule :: ParserConfig -> Text -> ([(SourceSpan, Text)], Module)
parseModule cfg input =
  let (errs, modu, _) = parseModuleWith id cfg input
   in (errs, modu)

-- | Parse a module and return the token trace with it.
--
-- The trace is each token that the parser took, in order. It contains real
-- tokens and virtual layout tokens, also the virtual close braces that the
-- parser inserts for the parse-error rule. Comments and pragmas are not in
-- the trace. A token that the parser took but did not keep in the AST, for
-- example a trailing comma, has 'traceDiscarded' set.
--
-- Two modules with the same AST have the same trace token kinds when the
-- discarded entries are removed. A formatter can use this to pair the tokens
-- of a source file with the tokens of its pretty-printed form.
parseModuleWithTrace :: ParserConfig -> Text -> ([(SourceSpan, Text)], Module, [TraceEntry])
parseModuleWithTrace = parseModuleWith enableTokStreamTrace

-- | Map a token kind to the kind that it has with spaces around it.
--
-- The lexer gives some operators a different kind when no space separates
-- them from the next or the previous token. For example, @(%1)@ has
-- 'TkPrefixPercent' but @(% 1)@ has @'TkVarSym' "%"@. The text and the AST
-- are the same, so compare trace entries with this function. It also maps
-- the linear arrow @⊸@ to @->@, because the trace of @%1 ->@ keeps only the
-- arrow.
tokenKindWithoutSpacing :: LexTokenKind -> LexTokenKind
tokenKindWithoutSpacing kind =
  case kind of
    TkPrefixMinus -> TkMinusOperator
    TkPrefixBang -> TkVarSym "!"
    TkPrefixTilde -> TkVarSym "~"
    TkPrefixPercent -> TkVarSym "%"
    TkRecordDot -> TkVarSym "."
    TkTypeApp -> TkVarSym "@"
    TkReservedAt -> TkVarSym "@"
    TkLinearArrow -> TkReservedRightArrow
    _ -> kind

parseModuleWith :: (TokStream -> TokStream) -> ParserConfig -> Text -> ([(SourceSpan, Text)], Module, [TraceEntry])
parseModuleWith prepare cfg input =
  case runTokStreamParser parser sourceName (prepare (mkTokStreamModule sourceName exts input)) of
    Left bundle ->
      ( parseErrorBundleToSpannedText sourceName errorStream bundle,
        Module
          { moduleAnns = [],
            moduleHead = Nothing,
            moduleLanguagePragmas = [],
            moduleImports = [],
            moduleDecls = []
          },
        []
      )
    Right (errs, modu, trace) ->
      (parseErrorsToSpannedText sourceName errorStream errs, modu, trace)
  where
    sourceName = parserSourceName cfg
    exts = applyImpliedExtensions (parserExtensions cfg)
    errorStream = rebuildStream (\(name, es, src) -> mkTokStreamModule name es src) (sourceName, exts, input)
    parser = do
      modu <- moduleParser
      errs <- drainParseErrors
      ts <- MP.getInput
      pure (errs, modu, maybe [] (reverse . traceEntries) (tokStreamTrace ts))

-- | Run a parser over freshly lexed input. Errors are located on a stream
-- built again from the input, so the parse itself does not retain the token
-- chain (see 'rebuildStream').
runEntry :: (FilePath -> [Extension] -> Text -> TokStream) -> TokParser a -> ParserConfig -> Text -> ParseResult a
runEntry mkStream parser cfg input =
  case runTokStreamParser parser sourceName (mkStream sourceName exts input) of
    Left bundle -> ParseErr (parseErrorBundleToSpannedText sourceName (rebuildStream (\(name, es, src) -> mkStream name es src) (sourceName, exts, input)) bundle)
    Right parsed -> ParseOk parsed
  where
    sourceName = parserSourceName cfg
    exts = applyImpliedExtensions (parserExtensions cfg)

-- | Pretty-print a list of spanned parse errors with source context.
formatParseErrors :: FilePath -> Maybe Text -> [(SourceSpan, Text)] -> String
formatParseErrors sourceName mSource errs =
  let opts = defaultLayoutOptions
      blocks =
        map
          ( \(srcSpan, msg) ->
              renderString
                ( layoutPretty opts $
                    case (srcSpan, mSource) of
                      (ss, Just source) ->
                        vcat [renderSourceReference source ss, pretty msg]
                      _ ->
                        vcat [pretty sourceName, pretty msg]
                )
          )
          errs
   in List.intercalate "\n\n" blocks

-- renderSourceReference "x = 1" (SourceSpan "<input>" 1 5 1 6 4 5) = """
-- <input>:1:5:
-- 1 | x = 1
--   |     ^
-- """
-- renderSourceReference "module where" (SourceSpan "<input>" 1 8 1 13 7 12) = """
-- <input>:1:8:
-- 1 | module where
--   |        ^^^^^
-- """
renderSourceReference :: Text -> SourceSpan -> Doc ann
renderSourceReference source srcSpan =
  let SourceSpan {sourceSpanSourceName = renderedOrigin, sourceSpanStartLine = lineNo, sourceSpanStartCol = colNo, sourceSpanEndCol = endCol, sourceSpanStartOffset} = srcSpan
      srcLine = extractSourceLineByOffset source sourceSpanStartOffset
      lineNoText = show lineNo
      markerPrefix = replicate (length lineNoText) ' ' ++ " | "
      markerStart = max 0 (colNo - 1)
      markerLen = max 1 (endCol - colNo)
      marker = replicate markerStart ' ' ++ replicate markerLen '^'
      header =
        pretty renderedOrigin <> colon <> pretty lineNo <> colon <> pretty colNo <> colon
   in vcat
        [ header,
          pretty (lineNoText ++ " | " ++ srcLine),
          pretty (markerPrefix ++ marker)
        ]

extractSourceLineByOffset :: Text -> Int -> String
extractSourceLineByOffset source offset =
  let bytes = TE.encodeUtf8 source
      len = BS.length bytes
      anchor = max 0 (min len offset)
      lineStart = scanBackward bytes anchor
      lineEnd = scanForward bytes anchor
   in T.unpack (TE.decodeUtf8 (BS.take (lineEnd - lineStart) (BS.drop lineStart bytes)))

scanBackward :: BS.ByteString -> Int -> Int
scanBackward bytes = go
  where
    go idx
      | idx <= 0 = 0
      | isLineBreak (BS.index bytes (idx - 1)) = idx
      | otherwise = go (idx - 1)

scanForward :: BS.ByteString -> Int -> Int
scanForward bytes = go
  where
    len = BS.length bytes
    go idx
      | idx >= len = len
      | isLineBreak (BS.index bytes idx) = idx
      | otherwise = go (idx + 1)

isLineBreak :: Word8 -> Bool
isLineBreak w = w == 10 || w == 13