Chapter 1

Parsing Haskell

How GHC turns a stream of characters into a syntax tree: the layout algorithm, a grammar that forbids its own ambiguities, and the trick that lets one production parse three different languages at once.

Where this lives in the tree

Parsing is the phase people are most tempted to skip. It sounds like solved territory: a lexer, a grammar, a tree. But Haskell hands its parser two problems most languages never face: indentation that carries meaning, and a syntax so ambiguous that the parser genuinely cannot tell an expression from a pattern until long after it has read both. The way GHC solves them shapes everything downstream.

By the end of this phase GHC has a HsModule GhcPs: a syntax tree that mirrors your source so faithfully it can be printed back out character for character, comments and all. Nothing has been resolved, checked, or simplified. Every name is still just a string in a namespace, and x + y is still three tokens rather than an application of anything.

Two tools, two generated files

GHC does not hand-write its lexer or its parser. Both are generated:

  • GHC/Parser/Lexer.x is processed by Alex, which compiles regular expressions into a table-driven scanner.
  • GHC/Parser.y is processed by Happy, which compiles a context-free grammar into an LALR shift-reduce parser.

Both files are large (the lexer runs to about 3,700 lines and the grammar to about 4,700), and both are mostly not regular expressions and productions. They are Haskell: the actions attached to each rule, plus a substantial preamble of supporting code. The generated tables are the small part.

The split matters more than it looks. A lexer that only recognised tokens could be a pure function from String to [Token]. GHC’s cannot, because Haskell’s layout rule means the tokens you emit depend on what you have already seen.

The parser monad

Both the lexer and the parser run in P, a state monad over PState:

newtype P a = P { unP :: PState -> ParseResult a }

PState carries what you would expect (the input StringBuffer, the current location, accumulated warnings and errors) and a few things you might not:

data PState = PState {
        buffer     :: StringBuffer,
        options    :: ParserOpts,
        warnings   :: Messages PsMessage,
        errors     :: Messages PsMessage,
        last_tk    :: Strict.Maybe (PsLocated Token),
        loc        :: PsLoc,       -- current loc (end of prev token + 1)
        context    :: [LayoutContext],
        lex_state  :: [Int],
        ...

Two fields there do the interesting work. context is the stack of open layout contexts, which we will come back to. And last_tk, the last non-comment token, exists because Haskell has a construct whose meaning depends on what preceded it, which we will also come back to.

The result type is a small performance flourish worth noticing, because it tells you how hot this code is:

newtype ParseResult a = PR (# (# PState, a #) | PState #)

pattern POk :: PState -> a -> ParseResult a
pattern POk s a = PR (# (# s , a #) | #)

pattern PFailed :: PState -> ParseResult a
pattern PFailed s = PR (# | s #)

That is an unboxed sum, hidden behind pattern synonyms so the rest of the compiler can pretend it is an ordinary two-constructor data type. Success or failure costs no allocation at all. GHC uses its own extensions on itself, and the parser is one of the places it does so most aggressively.

Layout: where indentation becomes syntax

Haskell lets you write this:

greet name = case name of
  Just n -> "hello, " ++ n
  Nothing -> "hello"

and the language definition says it means this:

greet name = case name of { Just n -> "hello, " ++ n ; Nothing -> "hello" }

Somebody has to insert those braces and semicolons, and that somebody is the lexer. When it emits a token that opens a block (where, let, do, of), it switches into a dedicated start state and pushes a new layout context recording the column the block began at:

data LayoutContext
  = NoLayout
  | Layout !Int !GenSemic

That Int is the reference column. Every subsequent line is compared against the top of the context stack: start at the same column and you get a virtual semicolon, start further left and the context is popped and you get a virtual close brace. The tokens are called ITvocurly, ITvccurly and ITsemi (virtual open and close curly), and they are what the grammar actually sees.

This is why the layout rule cannot live in the grammar. Happy sees a token stream in which the braces are already there; it never knows they were implied. And it is why the lexer needs state: the offside rule is a stack machine, not a regular language.

Whitespace, in a place you would not expect

Layout is the famous case of whitespace mattering. There is a second, subtler one. Consider:

f (!a) = ...     -- a bang pattern:  ! is a prefix operator here
g a ! b          -- an infix operator application

The same character means different things depending on the spaces around it. GHC’s rule, from the accepted proposal on whitespace-sensitive bang patterns, classifies every operator occurrence into one of four categories by looking at what sits immediately either side, which is exactly why PState bothers to remember last_tk.

Note [Whitespace-sensitive operator parsing] GHC/Parser/Lexer.x:637
In accord with GHC Proposal #229 https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0229-whitespace-bang-patterns.rst
we classify operator occurrences into four categories:

    a ! b   -- a loose infix occurrence
    a!b     -- a tight infix occurrence
    a !b    -- a prefix occurrence
    a! b    -- a suffix occurrence

The rules are a bit more elaborate than simply checking for whitespace, in
order to accommodate the following use cases:

    f (!a) = ...    -- prefix occurrence
    g (a !)         -- loose infix occurrence
    g (! a)         -- loose infix occurrence

The precise rules are as follows:

 * Identifiers, literals, and opening brackets (, (#, (|, [, [|, [||, [p|,
   [e|, [t|, {, ⟦, ⦇, are considered "opening tokens". The function
   followedByOpeningToken tests whether the next token is an opening token.

 * Identifiers, literals, and closing brackets ), #), |), ], |], }, ⟧, ⦈,
   are considered "closing tokens". The function precededByClosingToken tests
   whether the previous token is a closing token.

 * Whitespace, comments, separators, and other tokens, are considered
   neither opening nor closing.

 * Any unqualified operator occurrence is classified as prefix, suffix, or
   tight/loose infix, based on preceding and following tokens:

      precededByClosingToken | followedByOpeningToken | Occurrence
     ------------------------+------------------------+------------
      False                  | True                   | prefix
      True                   | False                  | suffix
      True                   | True                   | tight infix
      False                  | False                  | loose infix
     ------------------------+------------------------+------------

A loose infix occurrence is always considered an operator. Other types of
Show the rest of this Note (79 more lines)
occurrences may be assigned a special per-operator meaning override:

  Operator |  Occurrence   | Token returned
 ----------+---------------+------------------------------------------
   !       |  prefix       | ITbang
           |               |   strictness annotation or bang pattern,
           |               |   e.g.  f !x = rhs, data T = MkT !a
           |  not prefix   | ITvarsym "!"
           |               |   ordinary operator or type operator,
           |               |   e.g.  xs ! 3, (! x), Int ! Bool
 ----------+---------------+------------------------------------------
   ~       |  prefix       | ITtilde
           |               |   laziness annotation or lazy pattern,
           |               |   e.g.  f ~x = rhs, data T = MkT ~a
           |  not prefix   | ITvarsym "~"
           |               |   ordinary operator or type operator,
           |               |   e.g.  xs ~ 3, (~ x), Int ~ Bool
 ----------+---------------+------------------------------------------
   .       |  prefix       | ITproj True
           |               |   field projection,
           |               |   e.g.  .x
           |  tight infix  | ITproj False
           |               |   field projection,
           |               |   e.g. r.x
           |  suffix       | ITdot
           |               |   function composition,
           |               |   e.g. f. g
           |  loose infix  | ITdot
           |               |   function composition,
           |               |   e.g.  f . g
 ----------+---------------+------------------------------------------
   $  $$   |  prefix       | ITdollar, ITdollardollar
           |               |   untyped or typed Template Haskell splice,
           |               |   e.g.  $(f x), $$(f x), $$"str"
           |  not prefix   | ITvarsym "$", ITvarsym "$$"
           |               |   ordinary operator or type operator,
           |               |   e.g.  f $ g x, a $$ b
 ----------+---------------+------------------------------------------
   @       |  prefix       | ITtypeApp
           |               |   type application, e.g.  fmap @Maybe
           |  tight infix  | ITat
           |               |   as-pattern, e.g.  f p@(a,b) = rhs
           |  suffix       | parse error
           |               |   e.g. f p@ x = rhs
           |  loose infix  | ITvarsym "@"
           |               |   ordinary operator or type operator,
           |               |   e.g.  f @ g, (f @)
 ----------+---------------+------------------------------------------

Also, some of these overrides are guarded behind language extensions.
According to the specification, we must determine the occurrence based on
surrounding *tokens* (see the proposal for the exact rules). However, in
the implementation we cheat a little and do the classification based on
characters, for reasons of both simplicity and efficiency (see
'followedByOpeningToken' and 'precededByClosingToken')

When an operator is subject to a meaning override, it is mapped to special
token: ITbang, ITtilde, ITat, ITdollar, ITdollardollar. Otherwise, it is
returned as ITvarsym.

For example, this is how we process the (!):

   precededByClosingToken | followedByOpeningToken | Token
  ------------------------+------------------------+-------------
   False                  | True                   | ITbang
   True                   | False                  | ITvarsym "!"
   True                   | True                   | ITvarsym "!"
   False                  | False                  | ITvarsym "!"
  ------------------------+------------------------+-------------

And this is how we process the (@):

   precededByClosingToken | followedByOpeningToken | Token
  ------------------------+------------------------+-------------
   False                  | True                   | ITtypeApp
   True                   | False                  | parse error
   True                   | True                   | ITat
   False                  | False                  | ITvarsym "@"
  ------------------------+------------------------+-------------

The table in the middle of that Note is the whole rule. a ! b and a!b are both infix; a !b is prefix and a! b is suffix. Only then does GHC consult a per-operator override to decide which token to emit: ! in prefix position becomes ITbang, a strictness annotation, while a loose infix ! stays an ordinary operator.

A grammar that forbids its own ambiguities

Shift-reduce parsers have a characteristic failure mode: a state where the parser could either push the next token or reduce what it already has, and the choice changes the parse. Happy resolves these silently by default, which is a reasonable thing for a parser generator to do and a terrible thing for a language whose behaviour is a specification.

So Parser.y declares, near the top:

%expect 0

Zero unresolved shift-reduce conflicts. Every genuine ambiguity in Haskell’s grammar must be resolved explicitly, and the file documents each one. The canonical worked example is in the grammar’s own preamble:

Note [shift/reduce conflicts] GHC/Parser.y:106
The 'happy' tool turns this grammar into an efficient parser that follows the
shift-reduce parsing model. There's a parse stack that contains items parsed so
far (both terminals and non-terminals). Every next token produced by the lexer
results in one of two actions:

  SHIFT:    push the token onto the parse stack

  REDUCE:   pop a few items off the parse stack and combine them
            with a function (reduction rule)

However, sometimes it's unclear which of the two actions to take.
Consider this code example:

    if x then y else f z

There are two ways to parse it:

    (if x then y else f) z
    if x then y else (f z)

How is this determined? At some point, the parser gets to the following state:

  parse stack:  'if' exp 'then' exp 'else' "f"
  next token:   "z"

Scenario A (simplified):

  1. REDUCE, parse stack: 'if' exp 'then' exp 'else' exp
             next token:  "z"
        (Note that "f" reduced to exp here)

  2. REDUCE, parse stack: exp
             next token:  "z"

  3. SHIFT,  parse stack: exp "z"
             next token:  ...

  4. REDUCE, parse stack: exp
             next token:  ...

  This way we get:  (if x then y else f) z

Scenario B (simplified):
Show the rest of this Note (84 more lines)
  1. SHIFT,  parse stack: 'if' exp 'then' exp 'else' "f" "z"
             next token:  ...

  2. REDUCE, parse stack: 'if' exp 'then' exp 'else' exp
             next token:  ...

  3. REDUCE, parse stack: exp
             next token:  ...

  This way we get:  if x then y else (f z)

The end result is determined by the chosen action. When Happy detects this, it
reports a shift/reduce conflict. At the top of the file, we have the following
directive:

  %expect 0

It means that we expect no unresolved shift/reduce conflicts in this grammar.
If you modify the grammar and get shift/reduce conflicts, follow the steps
below to resolve them.

STEP ONE
  is to figure out what causes the conflict.
  That's where the -i flag comes in handy:

      happy -agc --strict compiler/GHC/Parser.y -idetailed-info

  By analysing the output of this command, in a new file `detailed-info`, you
  can figure out which reduction rule causes the issue. At the top of the
  generated report, you will see a line like this:

      state 147 contains 67 shift/reduce conflicts.

  Scroll down to section State 147 (in your case it could be a different
  state). The start of the section lists the reduction rules that can fire
  and shows their context:

        exp10 -> fexp .                 (rule 492)
        fexp -> fexp . aexp             (rule 498)
        fexp -> fexp . PREFIX_AT atype  (rule 499)

  And then, for every token, it tells you the parsing action:

        ']'            reduce using rule 492
        '::'           reduce using rule 492
        '('            shift, and enter state 178
        QVARID         shift, and enter state 44
        DO             shift, and enter state 182
        ...

  But if you look closer, some of these tokens also have another parsing action
  in parentheses:

        QVARID    shift, and enter state 44
                   (reduce using rule 492)

  That's how you know rule 492 is causing trouble.
  Scroll back to the top to see what this rule is:

        
        Grammar
        
        ...
        ...
        exp10 -> fexp                (492)
        optSemi -> ';'               (493)
        ...
        ...

  Hence the shift/reduce conflict is caused by this parser production:

        exp10 :: { ECP }
                : '-' fexp    { ... }
                | fexp        { ... }    -- problematic rule

STEP TWO
  is to mark the problematic rule with the %shift pragma. This signals to
  'happy' that any shift/reduce conflicts involving this rule must be resolved
  in favor of a shift. There's currently no dedicated pragma to resolve in
  favor of the reduce.

STEP THREE
  is to add a dedicated Note for this specific conflict, as is done for all
  other conflicts below.

Search the grammar for %shift and you will find the individual resolutions, each with its own Note: %shift: exp10 -> fexp, %shift: type -> btype, and a dozen more. They are among the most narrowly-scoped comments in GHC, and they exist because a future contributor will perturb the grammar and need to know which conflicts were deliberate.

When the grammar genuinely cannot decide

Conflicts you can resolve with a precedence declaration are the easy case. Haskell also has ambiguities that no amount of lookahead will fix, because the disambiguating token can be arbitrarily far away:

f (Con a b     ) = ...   -- 'Con a b' is a pattern
f (Con a b -> x) = ...   -- 'Con a b' is an expression

Until the parser reaches -> or =, it cannot know which language it is reading. The same shape recurs in do blocks, in guards, and (with a third possibility, arrow commands) inside proc.

The obvious fixes are all bad. Parsing both and backtracking is expensive and Happy will not do it. Parsing into a union type means every downstream consumer handles cases that cannot occur. Duplicating the grammar triples it.

GHC’s answer is to make the production polymorphic in what it is building:

Note [Ambiguous syntactic categories] GHC/Parser/PostProcess.hs:2498
There are places in the grammar where we do not know whether we are parsing an
expression or a pattern without unlimited lookahead (which we do not have in
'happy'):

View patterns:

    f (Con a b     ) = ...  -- 'Con a b' is a pattern
    f (Con a b -> x) = ...  -- 'Con a b' is an expression

do-notation:

    do { Con a b <- x } -- 'Con a b' is a pattern
    do { Con a b }      -- 'Con a b' is an expression

Guards:

    x | True <- p && q = ...  -- 'True' is a pattern
    x | True           = ...  -- 'True' is an expression

Top-level value/function declarations (FunBind/PatBind):

    f ! a         -- TH splice
    f ! a = ...   -- function declaration

    Until we encounter the = sign, we don't know if it's a top-level
    TemplateHaskell splice where ! is used, or if it's a function declaration
    where ! is bound.

There are also places in the grammar where we do not know whether we are
parsing an expression or a command:

    proc x -> do { (stuff) -< x }   -- 'stuff' is an expression
    proc x -> do { (stuff) }        -- 'stuff' is a command

    Until we encounter arrow syntax (-<) we don't know whether to parse 'stuff'
    as an expression or a command.

In fact, do-notation is subject to both ambiguities:

    proc x -> do { (stuff) -< x }        -- 'stuff' is an expression
    proc x -> do { (stuff) <- f -< x }   -- 'stuff' is a pattern
    proc x -> do { (stuff) }             -- 'stuff' is a command

There are many possible solutions to this problem. For an overview of the ones
we decided against, see Note [Resolving parsing ambiguities: non-taken alternatives]

The solution that keeps basic definitions (such as HsExpr) clean, keeps the
concerns local to the parser, and does not require duplication of hsSyn types,
Show the rest of this Note (61 more lines)
or an extra pass over the entire AST, is to parse into an overloaded
parser-validator (a so-called tagless final encoding):

    class DisambECP b where ...
    instance DisambECP (HsCmd GhcPs) where ...
    instance DisambECP (HsExp GhcPs) where ...
    instance DisambECP (PatBuilder GhcPs) where ...

The 'DisambECP' class contains functions to build and validate 'b'. For example,
to add parentheses we have:

  mkHsParPV :: DisambECP b => SrcSpan -> Located b -> PV (Located b)

'mkHsParPV' will wrap the inner value in HsCmdPar for commands, HsPar for
expressions, and 'PatBuilderPar' for patterns (later transformed into ParPat,
see Note [PatBuilder]).

Consider the 'alts' production used to parse case-of alternatives:

  alts :: { Located ([AddEpAnn],[LMatch GhcPs (LHsExpr GhcPs)]) }
    : alts1     { sL1 $1 (fst $ unLoc $1,snd $ unLoc $1) }
    | ';' alts  { sLL $1 $> ((mj AnnSemi $1:(fst $ unLoc $2)),snd $ unLoc $2) }

We abstract over LHsExpr GhcPs, and it becomes:

  alts :: { forall b. DisambECP b => PV (Located ([AddEpAnn],[LMatch GhcPs (Located b)])) }
    : alts1     { $1 >>= \ $1 ->
                  return $ sL1 $1 (fst $ unLoc $1,snd $ unLoc $1) }
    | ';' alts  { $2 >>= \ $2 ->
                  return $ sLL $1 $> ((mj AnnSemi $1:(fst $ unLoc $2)),snd $ unLoc $2) }

Compared to the initial definition, the added bits are:

    forall b. DisambECP b => PV ( ... ) -- in the type signature
    $1 >>= \ $1 -> return $             -- in one reduction rule
    $2 >>= \ $2 -> return $             -- in another reduction rule

The overhead is constant relative to the size of the rest of the reduction
rule, so this approach scales well to large parser productions.

Note that we write ($1 >>= \ $1 -> ...), so the second $1 is in a binding
position and shadows the previous $1. We can do this because internally
'happy' desugars $n to happy_var_n, and the rationale behind this idiom
is to be able to write (sLL $1 $>) later on. The alternative would be to
write this as ($1 >>= \ fresh_name -> ...), but then we couldn't refer
to the last fresh name as $>.

Finally, we instantiate the polymorphic type to a concrete one, and run the
parser-validator, for example:

    stmt   :: { forall b. DisambECP b => PV (LStmt GhcPs (Located b)) }
    e_stmt :: { LStmt GhcPs (LHsExpr GhcPs) }
            : stmt {% runPV $1 }

In e_stmt, three things happen:

  1. we instantiate: b ~ HsExpr GhcPs
  2. we embed the PV computation into P by using runPV
  3. we run validation by using a monadic production, {% ... }

At this point the ambiguity is resolved.

This is a tagless-final encoding. A single grammar production is written against the DisambECP class, and the instance chosen at the call site decides whether the result is an expression, a command, or a pattern. One production, three languages, no backtracking and no union type, at the cost of a layer of indirection that makes PostProcess.hs one of the harder files in the front end to read cold.

Nothing is thrown away

A parser that only needed to feed a compiler could discard comments, forget whether you wrote (a, b) or ( a , b ), and normalise away the parentheses it no longer needs. GHC cannot, because the same tree is used by tooling (HLint, Ormolu, ghc-exactprint, IDEs) that has to reproduce your source exactly.

Note [exact print annotations] GHC/Parser/Annotation.hs:117
Given a parse tree of a Haskell module, how can we reconstruct
the original Haskell source code, retaining all whitespace and
source code comments?  We need to track the locations of all
elements from the original source: this includes keywords such as
'let' / 'in' / 'do' etc as well as punctuation such as commas and
braces, and also comments.  We collectively refer to this
metadata as the "exact print annotations".

NON-COMMENT ELEMENTS

Intuitively, every AST element directly contains a bag of keywords
(keywords can show up more than once in a node: a semicolon i.e. newline
can show up multiple times before the next AST element), each of which
needs to be associated with its location in the original source code.

These keywords are recorded directly in the AST element in which they
occur, for the GhcPs phase.

For any given element in the AST, there is only a set number of
keywords that are applicable for it (e.g., you'll never see an
'import' keyword associated with a let-binding.)  The set of allowed
keywords is documented in a comment associated with the constructor
of a given AST element, although the ground truth is in GHC.Parser
and GHC.Parser.PostProcess (which actually add the annotations).

COMMENT ELEMENTS

We associate comments with the lowest (most specific) AST element
enclosing them
Show the rest of this Note (49 more lines)
PARSER STATE

There are three fields in PState (the parser state) which play a role
with annotation comments.

>  comment_q :: [LEpaComment],
>  header_comments :: Maybe [LEpaComment],
>  eof_pos :: Maybe (RealSrcSpan, RealSrcSpan), -- pos, gap to prior token

The 'comment_q' field captures comments as they are seen in the token stream,
so that when they are ready to be allocated via the parser they are
available.

The 'header_comments' capture the comments coming at the top of the
source file.  They are moved there from the `comment_q` when comments
are allocated for the first top-level declaration.

The 'eof_pos' captures the final location in the file, and the
location of the immediately preceding token to the last location, so
that the exact-printer can work out how far to advance to add the
trailing whitespace.

PARSER EMISSION OF ANNOTATIONS

The parser interacts with the lexer using the functions

> getCommentsFor      :: (MonadP m) => SrcSpan -> m EpAnnComments
> getPriorCommentsFor :: (MonadP m) => SrcSpan -> m EpAnnComments
> getFinalCommentsFor :: (MonadP m) => SrcSpan -> m EpAnnComments

The 'getCommentsFor' function is the one used most often.  It takes
the AST element SrcSpan and removes and returns any comments in the
'comment_q' that are inside the span. 'allocateComments' in 'Lexer' is
responsible for making sure we only return comments that actually fit
in the 'SrcSpan'.

The 'getPriorCommentsFor' function is used for top-level declarations,
and removes and returns any comments in the 'comment_q' that either
precede or are included in the given SrcSpan. This is to ensure that
preceding documentation comments are kept together with the
declaration they belong to.

The 'getFinalCommentsFor' function is called right at the end when EOF
is hit. This drains the 'comment_q' completely, and returns the
'header_comments', remaining 'comment_q' entries and the
'eof_pos'. These values are inserted into the 'HsModule' AST element.

The wiki page describing this feature is
https://gitlab.haskell.org/ghc/ghc/wikis/api-annotations

So the tree carries the source span of every keyword, bracket and comma, plus the comments and where they attached. This is a large part of why the parsed AST dump below is so much bigger than the program that produced it.

Seeing it happen

Here is a small module exercising layout, an operator section, a user-defined infix operator, and do notation. Step through the stages: the first two tabs are what this chapter produced.

What you wrote.

-- | A deliberately small module aimed at the *front* of the pipeline.
--
-- The point of interest here is the parsed AST rather than the Core: layout
-- (no braces or semicolons are written, yet the parser inserts them),
-- operator sections, `do` notation, and an infix operator whose fixity is not
-- known until the renamer runs.
module Syntax where

infixl 6 |+|

(|+|) :: Int -> Int -> Int
x |+| y = x + y * 2

total :: [Int] -> Int
total xs = sum (map (|+| 1) xs)

greet :: Maybe String -> String
greet name = case name of
  Just n -> "hello, " ++ n
  Nothing -> "hello"

collect :: [Int] -> [Int]
collect xs = do
  x <- xs
  let doubled = x * 2
  pure doubled
The Parsed tab is the AST pretty-printed back to Haskell: note the braces and semicolons that layout inserted, and that (|+| 1) is still an unresolved section.

Two things are worth pausing on.

In Parsed, the layout you wrote has become explicit braces and semicolons: that is the offside rule’s output, made visible. And in Parsed AST, notice how much of the bulk is EpAnn and Anchor nodes recording exact source positions: the annotations from the previous section, in the flesh.

Notice also what has not happened. x |+| y * 2 sits in the tree as a flat OpApp chain with no grouping, even though |+| is declared infixl 6 right there in the file. Fixity resolution needs to know the fixity of every operator in scope, including imported ones, and that information does not exist yet. It arrives with the renamer, in the next chapter.

What the parser hands on

The output is HsModule GhcPs. That GhcPs is a phase index: the same AST types are reused by the renamer as GhcRn and the typechecker as GhcTc, with different field types at each stage. The technique is called Trees That Grow, and it is why GHC/Hs/Expr.hs is full of type families rather than concrete fields.

At GhcPs, every identifier is a RdrName: a name exactly as written, qualified or not, with no idea what it refers to. Turning those into Names that point at actual entities is the renamer’s job.

Reading the source yourself

If you want to work on the front end, a reasonable order is:

  1. GHC/Parser/Lexer.x, bottom half first. The Haskell preamble (P, PState, new_layout_context, maybe_layout) is more approachable than the Alex rules above it, and explains what those rules are doing.
  2. GHC/Parser.y, starting with its Notes rather than its productions. The preamble Notes explain the conventions the 4,700 lines below them follow.
  3. GHC/Parser/PostProcess.hs last, once DisambECP has a reason to exist.

The parser is also the easiest phase in which to make a first contribution: its behaviour is observable from the outside with -ddump-parsed-ast, its tests are plain source files, and a change here cannot silently miscompile anything. Either it parses or it does not.