How do I get position/span info for pretty-printed data types?

I’m implementing a toy programming language. I want to write a function that performs a one-step evaluation on a given term and return fancy errors that annotate which part of the term raised the error. How do I do that? So imagine we have an AST:

data Term = If Term Term Term
          | IsZero Term
          | AddOne Term
          | Num Int
          | MyTrue | MyFalse
          deriving (Show)

and I have a function that evaluates a term and returns a fancy error (String):

eval :: Term -> Either String Term

This function performs one-step evaluation, so it makes a small progress:

eval $ If (IsZero (AddOne (Num 14))) (Num 100) (Num 120)
--  should be:
--  Right (If (IsZero (Num 15)) (Num 100) (Num 120))

and when it fails, it should return an error message that (1) pretty-prints the term, and (2) indicates which part of the term was responsible for the error:

main = do
  let res = eval $ If (IsZero (AddOne MyFalse)) (Num 100) (Num 120)
  case res of 
    Right term -> putStrLn $ show term
    Left err -> putStrLn err
--  should print:
--  if (iszero (addone myfalse)) 100 120
--                     ------- Expected number, got something else

I can implement pretty-printing (1) using prettyprinter. But to do (2), the eval function will need access to the spans for the pretty-printed terms (and any of its subterms). This means I will probably need another datatype that represents terms enriched with their span info:

--  A span is ((beginRow, beginCol), (endRow, endCol))
type Span = ((Int, Int), (Int, Int))
data SpannedTerm = If Span SpannedTerm SpannedTerm SpannedTerm
          | IsZero Span SpannedTerm
          | AddOne Span SpannedTerm
          | Num Span Int
          | MyTrue Span | MyFalse Span
          deriving (Show)

I will probably need a function that converts a term into a pretty-printed representation and a spanned term:

convert :: Term -> (String, SpannedTerm)>

and the eval function needs to take a spanned term and return a normal term:

eval' :: SpannedTerm -> Either String Term

My problem is: how do we write convert? Does prettyprinter have any kind of feature that would help me get the span information for each (sub)term? On the readme for prettyprinter it says:

More complex uses of annotations include e.g. … adding source locations to show where a certain piece of output comes from. Idris is a project that makes extensive use of such a feature.

So I wonder maybe it is possible to get what I want? Note that these span information cannot come from a parser because (1) eval keeps changing the term and (2) the span information depends on how prettyprinter layouts the term.

1 Like

You might be able to get some mileage out column :: (Int → Doc ann) → Doc ann which lets you know what column a particular doc is being laid out at.

An important limitation of it though is that you can’t smuggle the column out of the doc that is being rendered, which makes it probably not quite sufficient for your usecase.

I’d recommend looking into what Idris actually does + see if their behavior is sufficient for what you want to achieve. You may be stuck writing your own custom pretty printer that comes with source information if you want to do something sufficiently fancy.