packages feed

rtk-0.11: Diagnostics.hs

-- | Structured, user-facing diagnostics for the grammar-processing pipeline.
--
-- Errors caused by a bad grammar are represented as 'Diagnostic' values that
-- are threaded through 'Either' instead of being thrown with 'error'. The
-- pipeline stages (lexing, parsing, normalization, code generation) return
-- @Either Diagnostic a@; the executable renders the diagnostic in GNU one-line
-- style and exits, so RTK never shows a Haskell exception for a user mistake.
--
-- This module sits below the rest of the project (it imports nothing from it),
-- so 'SourcePos' lives here and both the lexer and the parser can refer to it.
module Diagnostics
    ( SourcePos(..)
    , showSourcePos
    , Diagnostic(..)
    , renderDiagnostic
    , diagnosticFromPositioned
    ) where

import Data.Data (Data)

-- | Position in the grammar source file (line and column, both 1-based).
data SourcePos = SourcePos { srcLine :: Int, srcColumn :: Int }
                 deriving (Eq, Ord, Show, Data)

showSourcePos :: SourcePos -> String
showSourcePos (SourcePos line col) = "line " ++ show line ++ ", column " ++ show col

-- | A user-facing diagnostic: an optional source position, optional context
-- (such as the rule being processed) and the message itself.
--
-- The 'Data' instance lets @--profile-stages@ deep-force an @Either Diagnostic
-- a@ result like any other pipeline value.
data Diagnostic = Diagnostic
  { diagPos     :: Maybe SourcePos
  , diagContext :: Maybe String    -- ^ e.g. @"in rule 'Foo'"@
  , diagMessage :: String
  } deriving (Eq, Show, Data)

-- | Render a diagnostic in GNU one-line style:
--
-- >>> renderDiagnostic "g.pg" (Diagnostic (Just (SourcePos 2 1)) (Just "in rule 'Foo'") "message")
-- "g.pg:2:1: error: in rule 'Foo': message"
--
-- The position is omitted when unknown and the context when absent:
--
-- >>> renderDiagnostic "g.pg" (Diagnostic Nothing Nothing "message")
-- "g.pg: error: message"
renderDiagnostic :: FilePath -> Diagnostic -> String
renderDiagnostic file (Diagnostic mpos mctx msg) =
    file ++ posPart ++ ": error: " ++ ctxPart ++ msg
  where
    posPart = case mpos of
                Just (SourcePos line col) -> ":" ++ show line ++ ":" ++ show col
                Nothing                   -> ""
    ctxPart = case mctx of
                Just ctx -> ctx ++ ": "
                Nothing  -> ""

-- | Recover a structured diagnostic from an error message that encodes its
-- position as @\"LINE:COL:message\"@ — the machine-splittable encoding shared
-- by the hand-written lexer and by every generated lexer and parser. Falls
-- back to a position-less diagnostic when the encoding is absent (e.g. an
-- internal alex error).
diagnosticFromPositioned :: String -> Diagnostic
diagnosticFromPositioned err =
    case span (/= ':') err of
        (l, ':' : rest1) | [(line, "")] <- reads l ->
            case span (/= ':') rest1 of
                (c, ':' : msg) | [(col, "")] <- reads c ->
                    Diagnostic (Just (SourcePos line col)) Nothing msg
                _ -> noPos
        _ -> noPos
  where noPos = Diagnostic Nothing Nothing err