packages feed

rtk-0.12: src/generated/Frontend.hs

{-# LANGUAGE QuasiQuotes #-}
-- | The front-end surface of the pipeline. Since task 8c the pipeline
-- computes directly over the GENERATED AST ('GrammarParser''s types, compiled
-- from the @test/golden/grammar/@ snapshot): there is no separate
-- hand-written grammar representation anymore. This module hosts what both
-- front ends share:
--
--   * the default (self-hosted) entry points: 'parseWithGenerated' lexes and
--     parses a grammar source with the lexer\/parser RTK generated from
--     @test-grammars/grammar.pg@;
--
--   * 'cleanGrammarTokens', the ONE implementation of token-level cleanup.
--     Both lexers keep the full token text (quotes around @'str'@, brackets
--     around @[regex]@, triple quotes around @\"\"\"bigstr\"\"\"@, escape
--     pairs intact), and both front ends run this pass right after parsing:
--     it strips the literal delimiters and applies
--     'TokenProcessing.unBackQuote' to string and regex leaves. The escape
--     logic is not duplicated anywhere else.
--
--   * the accessors and spine flatteners the pipeline stages
--     ('StringLiterals', 'Normalize', the generators, 'Debug') use to
--     compute over @GP.Grammar@\/@GP.Rule@\/@GP.Clause@ values, including
--     position projection into the diagnostics' 'SourcePos'.
--
-- Every generated constructor (except the quasi-quotation-only @Anti_*@
-- ones) carries the source position of its first symbol in its first field,
-- so diagnostics can point at the offending clause, not just the rule
-- header. Positions are equality-transparent ('GP.RtkPos'), which is why the
-- AST-equality harness projects them explicitly via 'sourcePosOf'.
--
-- Clause-shape matches here and in the later pipeline stages are written
-- with rtk's own generated quasi-quoter where that reads better than the
-- constructor spelling (task 8d): a @[clause| ... |]@ pattern is the clause
-- shape in grammar syntax, with @$cl...@ metavariables binding sub-clauses
-- (the names resolve through grammar.pg's @\@shortcuts@ table, so they must
-- start with a declared shortcut such as @cl@, or use the explicit
-- @$Clause:name@ form) and every position wildcarded.
module Frontend
    ( -- * Parsing entry points (the generated front end)
      parseWithGenerated
    , scanTokensGenerated
      -- * Token-level cleanup shared by both front ends
    , cleanGrammarTokens
      -- * Grammar accessors
    , grammarName
    , grammarImports
    , grammarRules
    , mapGrammarRules
      -- * Rule accessors
    , ruleName
    , ruleTypeName
    , ruleFunc
    , ruleClause
    , ruleOptions
    , rulePos
      -- * Clause helpers
    , altElems
    , seqElems
    , nameText
    , strLitText
    , showClause
    , showClauseSeq
      -- * Positions
    , sourcePosOf
    , clausePos
    , namePos
    ) where

import Data.Generics (everywhere, extT, mkT)
import Data.List (intercalate)

import qualified GrammarLexer as GL
import qualified GrammarParser as GP
import GrammarQQ (clause)

import Diagnostics (Diagnostic, SourcePos(..), diagnosticFromPositioned)
import TokenProcessing (unBackQuote)

-- | Lex and parse a grammar source with the generated front end and clean the
-- token text. The generated lexer and parser encode error positions as
-- @\"LINE:COL:message\"@ (the same encoding the hand-written lexer uses), so
-- their failures are split back into a positioned 'Diagnostic' here and
-- render with the same GNU-style @FILE:LINE:COL:@ prefix as the hand-written
-- front end's.
parseWithGenerated :: String -> Either Diagnostic GP.Grammar
parseWithGenerated src =
    case GL.scanTokens src >>= GP.parseGrammar of
        Left msg -> Left (diagnosticFromPositioned msg)
        Right g  -> Right (cleanGrammarTokens g)

-- | The generated lexer's token stream, rendered one token per line for
-- @--debug-tokens@. Token text is the raw match ('cleanGrammarTokens' runs
-- on the AST after parsing), so this is the stream exactly as the parser
-- consumes it.
scanTokensGenerated :: String -> Either Diagnostic [String]
scanTokensGenerated src =
    case GL.scanTokens src of
        Left msg   -> Left (diagnosticFromPositioned msg)
        Right toks -> Right (map show toks)

-- | Strip literal delimiters and process escapes on every token-text leaf of
-- a parsed grammar: quotes around string literals, brackets around regex
-- literals (both with 'unBackQuote' applied to the content) and the triple
-- quotes around the @imports@ block. Both front ends apply this right after
-- parsing; everything downstream sees clean text.
cleanGrammarTokens :: GP.Grammar -> GP.Grammar
cleanGrammarTokens = everywhere (mkT cleanStr `extT` cleanClause `extT` cleanTop)
  where
    cleanStr (GP.Str p s)  = GP.Str p (unBackQuote (stripEnds 1 s))
    cleanStr s             = s
    cleanClause (GP.Regex p s) = GP.Regex p (unBackQuote (stripEnds 1 s))
    cleanClause c              = c
    cleanTop (GP.GrammarImports p n imp rs) = GP.GrammarImports p n (stripEnds 3 imp) rs
    cleanTop g                              = g

-- | Drop @n@ delimiter characters from both ends of a token's text.
stripEnds :: Int -> String -> String
stripEnds n s = take (length s - 2 * n) (drop n s)

--------------------------------------------------------------------------------
-- Grammar accessors
--------------------------------------------------------------------------------

grammarName :: GP.Grammar -> String
grammarName (GP.GrammarDef _ n _)       = strLitText n
grammarName (GP.GrammarImports _ n _ _) = strLitText n
grammarName g                           = qqOnly "Grammar" g

grammarImports :: GP.Grammar -> String
grammarImports (GP.GrammarImports _ _ imp _) = imp
grammarImports (GP.GrammarDef _ _ _)         = ""
grammarImports g                             = qqOnly "Grammar" g

grammarRules :: GP.Grammar -> [GP.Rule]
grammarRules (GP.GrammarDef _ _ rs)       = rs
grammarRules (GP.GrammarImports _ _ _ rs) = rs
grammarRules g                            = qqOnly "Grammar" g

mapGrammarRules :: ([GP.Rule] -> [GP.Rule]) -> GP.Grammar -> GP.Grammar
mapGrammarRules f (GP.GrammarDef p n rs)         = GP.GrammarDef p n (f rs)
mapGrammarRules f (GP.GrammarImports p n imp rs) = GP.GrammarImports p n imp (f rs)
mapGrammarRules _ g                              = qqOnly "Grammar" g

--------------------------------------------------------------------------------
-- Rule accessors (transparent over the RuleWithOptions wrapper)
--------------------------------------------------------------------------------

ruleName :: GP.Rule -> String
ruleName (GP.RuleSimple _ n _)        = nameText n
ruleName (GP.RuleTyped _ _ n _)       = nameText n
ruleName (GP.RuleTypedFunc _ _ _ n _) = nameText n
ruleName (GP.RuleFunc _ _ n _)        = nameText n
ruleName (GP.RuleWithOptions _ _ r)   = ruleName r
ruleName r                            = qqOnly "Rule" r

ruleTypeName :: GP.Rule -> Maybe String
ruleTypeName (GP.RuleSimple _ _ _)        = Nothing
ruleTypeName (GP.RuleTyped _ t _ _)       = Just (nameText t)
ruleTypeName (GP.RuleTypedFunc _ t _ _ _) = Just (nameText t)
ruleTypeName (GP.RuleFunc _ _ _ _)        = Nothing
ruleTypeName (GP.RuleWithOptions _ _ r)   = ruleTypeName r
ruleTypeName r                            = qqOnly "Rule" r

ruleFunc :: GP.Rule -> Maybe String
ruleFunc (GP.RuleSimple _ _ _)        = Nothing
ruleFunc (GP.RuleTyped _ _ _ _)       = Nothing
ruleFunc (GP.RuleTypedFunc _ _ f _ _) = Just (nameText f)
ruleFunc (GP.RuleFunc _ f _ _)        = Just (nameText f)
ruleFunc (GP.RuleWithOptions _ _ r)   = ruleFunc r
ruleFunc r                            = qqOnly "Rule" r

ruleClause :: GP.Rule -> GP.Clause
ruleClause (GP.RuleSimple _ _ c)        = c
ruleClause (GP.RuleTyped _ _ _ c)       = c
ruleClause (GP.RuleTypedFunc _ _ _ _ c) = c
ruleClause (GP.RuleFunc _ _ _ c)        = c
ruleClause (GP.RuleWithOptions _ _ r)   = ruleClause r
ruleClause r                            = qqOnly "Rule" r

ruleOptions :: GP.Rule -> [GP.Option]
ruleOptions (GP.RuleWithOptions _ opts r) = opts ++ ruleOptions r
ruleOptions _                             = []

-- | A rule's position is where the rule header starts (for the @.Func:@ form
-- that is the dot itself), skipping the options wrapper - the same position
-- the pipeline reported back when it tracked rule positions explicitly.
rulePos :: GP.Rule -> Maybe SourcePos
rulePos (GP.RuleWithOptions _ _ r) = rulePos r
rulePos r                          = sourcePosOf (GP.rtkPosOf r)

--------------------------------------------------------------------------------
-- Clause helpers
--------------------------------------------------------------------------------

-- | Flatten the left-recursive @'|'@ spine into source-order alternatives.
-- The right operand of each node is a single alternative, never another
-- alternation (parenthesized alternations stay as elements).
altElems :: GP.Clause -> [GP.Clause]
altElems [clause| $cl1 | $cl2 |] = altElems cl1 ++ [cl2]
altElems c                       = [c]

-- | Flatten the left-recursive juxtaposition spine of one alternative.
seqElems :: GP.Clause -> [GP.Clause]
seqElems [clause| $cl1 $cl2 |] = seqElems cl1 ++ [cl2]
seqElems c                     = [c]

nameText :: GP.Name -> String
nameText (GP.Ident _ s)   = s
nameText n@GP.Anti_Name{} = qqOnly "Name" n

strLitText :: GP.StrLit -> String
strLitText (GP.Str _ s)       = s
strLitText s@GP.Anti_StrLit{} = qqOnly "StrLit" s

-- Render a clause the way it appears in the grammar source, for error messages
showClause :: GP.Clause -> String
showClause (GP.Ref _ n)         = nameText n
showClause (GP.Lit _ s)         = "'" ++ strLitText s ++ "'"
showClause (GP.Dot _)           = "."
showClause (GP.Regex _ s)       = "[" ++ s ++ "]"
showClause (GP.Star _ c)        = showClause c ++ "*"
showClause (GP.StarDelim _ c d) = showClause c ++ "* ~ " ++ showClause d
showClause (GP.Plus _ c)        = showClause c ++ "+"
showClause (GP.PlusDelim _ c d) = showClause c ++ "+ ~ " ++ showClause d
showClause (GP.Opt _ c)         = showClause c ++ "?"
showClause (GP.Lifted _ c)      = "," ++ showClause c
showClause (GP.Ignored _ c)     = "!" ++ showClause c
showClause (GP.Labeled _ n c)   = nameText n ++ ": " ++ showClause c
showClause c@GP.Alt{}           = "(" ++ intercalate " | " (map showClause (altElems c)) ++ ")"
showClause c@GP.Seq{}           = showClauseSeq (seqElems c)
showClause (GP.Anti_Clause v)   = "$" ++ v

-- | Render one alternative's element list (a flattened sequence).
showClauseSeq :: [GP.Clause] -> String
showClauseSeq = unwords . map showClause

--------------------------------------------------------------------------------
-- Positions
--------------------------------------------------------------------------------

-- | Project a generated-AST position into the diagnostics' 'SourcePos'.
-- 'GP.rtkNoPos' (synthesized nodes, @Anti_*@ splices) projects to 'Nothing'.
sourcePosOf :: GP.RtkPos -> Maybe SourcePos
sourcePosOf (GP.RtkPos (GL.AlexPn _ line col))
    | line <= 0 = Nothing
    | otherwise = Just (SourcePos line col)

clausePos :: GP.Clause -> Maybe SourcePos
clausePos = sourcePosOf . GP.rtkPosOf

namePos :: GP.Name -> Maybe SourcePos
namePos = sourcePosOf . GP.rtkPosOf

-- | The generated grammar also contains constructors that only quasi-quote
-- splices can produce: @Anti_*@ metavariables and the start-rule wrappers
-- around dummy tokens. Parsing a grammar source file cannot reach them, so
-- they are an internal error rather than a diagnostic.
qqOnly :: Show a => String -> a -> b
qqOnly ty v = error $ "Frontend: quasi-quotation-only " ++ ty
                   ++ " constructor cannot come from a grammar file: " ++ show v