packages feed

technique-0.2.5: lib/Technique/Failure.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

-- I generally try to avoid modules full of (only) types but these are here
-- so the can be shared in both Technique.Translate and Technique.Builtins.

-- |
-- Error messages from compiling.
module Technique.Failure where

import Core.System.Base
import Core.System.Pretty
import Core.Text.Rope
import Core.Text.Utilities
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Set as OrdSet
import qualified Data.Text as T
import Data.Void
import Technique.Formatter
import Technique.Language hiding (Label)
import Text.Megaparsec (PosState (..), SourcePos (..))
import Text.Megaparsec.Error
  ( ErrorItem (..),
    ParseError (..),
    ParseErrorBundle (..),
  )
import Text.Megaparsec.Pos (unPos)
import Prelude hiding (lines)

data Status = Ok | Failed CompilationError | Reload

instance Render Status where
  type Token Status = TechniqueToken
  colourize = colourizeTechnique
  highlight status = case status of
    Ok -> annotate LabelToken "ok"
    Failed e -> highlight e
    Reload -> annotate MagicToken "Δ"

data Source = Source
  { sourceContents :: Rope,
    sourceFilename :: FilePath,
    sourceOffset :: !Offset
  }
  deriving (Eq, Ord, Show)

instance Located Source where
  locationOf = sourceOffset

instance Render Source where
  type Token Source = TechniqueToken
  colourize = colourizeTechnique
  highlight source = pretty (sourceFilename source) <+> pretty (sourceOffset source)

emptySource :: Source
emptySource =
  Source
    { sourceContents = emptyRope,
      sourceFilename = "<undefined>",
      sourceOffset = -1
    }

data FailureReason
  = InvalidSetup -- TODO placeholder
  | ParsingFailed [ErrorItem Char] [ErrorItem Char]
  | VariableAlreadyInUse Identifier
  | ProcedureAlreadyDeclared Identifier
  | CallToUnknownProcedure Identifier
  | UseOfUnknownIdentifier Identifier
  | EncounteredUndefined
  deriving (Show, Eq)

instance Enum FailureReason where
  fromEnum x = case x of
    InvalidSetup -> 1
    ParsingFailed _ _ -> 2
    VariableAlreadyInUse _ -> 3
    ProcedureAlreadyDeclared _ -> 4
    CallToUnknownProcedure _ -> 5
    UseOfUnknownIdentifier _ -> 6
    EncounteredUndefined -> 7
  toEnum = undefined

data CompilationError = CompilationError Source FailureReason
  deriving (Show)

instance Exception CompilationError

exitCodeFor :: CompilationError -> Int
exitCodeFor (CompilationError _ reason) = fromEnum reason

-- TODO upgrade this to (Doc ann) so we can get prettier error messages.

instance Render FailureReason where
  type Token FailureReason = TechniqueToken
  colourize = colourizeTechnique
  highlight failure = case failure of
    InvalidSetup -> "Invalid setup!"
    ParsingFailed unexpected expected ->
      let un = case unexpected of
            [] -> emptyDoc
            (item : _) -> "unexpected " <> formatErrorItem FilenameToken item <> hardline
          ex = case expected of
            [] -> emptyDoc
            items -> "expecting " <> fillCat (fancyPunctuate (fmap (formatErrorItem SymbolToken) items)) <> "."
       in un <> ex
    VariableAlreadyInUse i -> "Variable by the name of '" <> annotate VariableToken (highlight i) <> "' already defined."
    ProcedureAlreadyDeclared i -> "Procedure by the name of '" <> annotate ProcedureToken (highlight i) <> "' already declared."
    CallToUnknownProcedure i -> "Call to unknown procedure '" <> annotate ApplicationToken (highlight i) <> "'."
    UseOfUnknownIdentifier i -> "Variable '" <> annotate VariableToken (highlight i) <> "' not in scope."
    EncounteredUndefined -> "Encountered an " <> annotate ErrorToken "undefined" <> " marker."

fancyPunctuate :: [Doc ann] -> [Doc ann]
fancyPunctuate list = case list of
  [] -> []
  [x] -> [x]
  (x1 : x2 : []) -> x1 : ", or " : x2 : []
  (x1 : xs) -> x1 : ", " : fancyPunctuate xs

-- |
-- ErrorItem is a bit overbearing, but we handle its /four/ cases by saying
-- single quotes around characters, double quotes around strings, /no/ quotes
-- around labels (descriptive text) and hard code the end of input and newline
-- cases.
formatErrorItem :: TechniqueToken -> ErrorItem Char -> Doc TechniqueToken
formatErrorItem token item = case item of
  -- It would appear that **prettyprinter** has a Pretty instance for
  -- NonEmpty a. In this case token ~ Char so these are Strings, ish.
  -- Previously we converted to Rope, but looks like we can go directly.

  Tokens tokens ->
    case NonEmpty.uncons tokens of
      (ch, Nothing) -> case ch of
        '\n' -> annotate token "newline"
        _ -> pretty '\'' <> annotate token (pretty ch) <> pretty '\''
      _ -> pretty '\"' <> annotate token (pretty tokens) <> pretty '\"'
  Label chars ->
    annotate token (pretty chars)
  EndOfInput ->
    "end of input"

numberOfCarots :: FailureReason -> Int
numberOfCarots reason = case reason of
  InvalidSetup -> 0
  ParsingFailed unexpected _ -> case unexpected of
    [] -> 1
    (item : _) -> case item of
      Tokens tokens -> NonEmpty.length tokens
      Label chars -> NonEmpty.length chars
      EndOfInput -> 1
  VariableAlreadyInUse i -> widthRope (unIdentifier i)
  ProcedureAlreadyDeclared i -> widthRope (unIdentifier i)
  CallToUnknownProcedure i -> widthRope (unIdentifier i)
  UseOfUnknownIdentifier i -> widthRope (unIdentifier i)
  EncounteredUndefined -> 1

instance Render CompilationError where
  type Token CompilationError = TechniqueToken
  colourize = colourizeTechnique
  highlight (CompilationError source reason) =
    let filename = pretty (sourceFilename source)
        contents = intoRope (sourceContents source)
        o = sourceOffset source
        -- Given an offset point where the error occured, split the input at that
        -- point.

        (before, _) = splitRope o contents
        (l, c) = calculatePositionEnd before
        -- Isolate the line on which the error occured. l and c are 1-origin here,
        -- so if there's only a single line (or empty file) we take that one single
        -- line and then last one is also that line.

        lines = breakLines contents
        lines' = take l lines
        offending =
          if nullRope contents
            then emptyRope
            else last lines'
        -- Now prepare for rendering. If the offending line is long trim it. Then
        -- create a line with some carets which show where the problem is.

        linenum = pretty l
        colunum = pretty c
        (truncated, _) = splitRope 77 offending
        trimmed =
          if widthRope offending > 77 && c < 77
            then truncated <> "..."
            else offending
        padding = replicateChar (c - 1) ' '
        num = numberOfCarots reason
        caroted = replicateChar num '^'
        columns =
          if num > 1
            then colunum <> "-" <> pretty (c + num - 1)
            else colunum
     in annotate FilenameToken filename <> ":" <> linenum <> ":" <> columns <> hardline
          <> hardline
          <> pretty trimmed
          <> hardline
          <> pretty padding
          <> annotate ErrorToken (pretty caroted)
          <> hardline
          <> hardline
          <> highlight reason

-- |
-- When we get a failure in the parsing stage **megaparsec** returns a
-- ParseErrorBundle. Extract the first error message therein (later handle
-- more? Yeah nah), and convert it into something we can use.
extractErrorBundle :: Source -> ParseErrorBundle T.Text Void -> CompilationError
extractErrorBundle source bundle =
  let errors = bundleErrors bundle
      first = NonEmpty.head errors
      (o, unexpected, expected) = extractParseError first
      pstate = bundlePosState bundle
      srcpos = pstateSourcePos pstate
      l0 = unPos . sourceLine $ srcpos
      c0 = unPos . sourceColumn $ srcpos
      -- Do we need these? For all the examples we have seen the values of l0 and c0
      -- are `1`. **megaparsec** delays calculation of line and column until
      -- error rendering time. Perhaps we need to record this.

      l = if l0 > 1 then error "Unexpected line balance" else 0
      c = if c0 > 1 then error "Unexpected columns balance" else 0
      reason = ParsingFailed unexpected expected
      source' =
        source
          { sourceOffset = o + l + c
          }
   in CompilationError source' reason

extractParseError :: ParseError T.Text Void -> (Int, [ErrorItem Char], [ErrorItem Char])
extractParseError e = case e of
  TrivialError o unexpected0 expected0 ->
    let unexpected = case unexpected0 of
          Just item -> item : []
          Nothing -> []
        expected = OrdSet.toList expected0
     in (o, unexpected, expected)
  FancyError _ _ -> error "Unexpected parser error"