purekell-0.1.0.0: src/Purekell/Printer.hs
-- | Pretty-printers for the shared AST.
--
-- Converts AST nodes back to source text for a given 'Target' language.
-- The printer handles all divergent syntax automatically:
--
-- * __Record access__: @field rec@ (Haskell) vs @rec.field@ (PureScript)
-- * __Tuples__: @(a, b)@ (Haskell) vs @Tuple a b@ (PureScript)
-- * __Cons patterns__: @x : xs@ (Haskell) vs @Cons x xs@ (PureScript)
-- * __Record field separators__: @=@ (Haskell) vs @:@ (PureScript, constructor context)
--
-- The printer inserts parentheses as needed to preserve meaning.
module Purekell.Printer
( -- * Target language
Target (..)
-- * Operator printing
, printOp
-- * Expression printing
, printExpr
, printLit
-- * Pattern printing
, printPat
, printPatAtom
-- * Parenthesization helpers
, printAtom
, printAppFun
, printInfixArg
, printInfixLevel
-- * Guard, case alt, binding, and statement printing
, printGuard
, printGuards
, printCaseAlt
, printBinding
, printStmt
-- * Type printing
, printType
) where
import Data.Text (Text)
import qualified Data.Text as T
import Purekell.AST
-- | Target language for printing. Determines syntax choices for
-- divergent constructs like record access, tuples, and cons patterns.
data Target = Haskell | PureScript
deriving (Eq, Show)
-- Qualified name helper
printQual :: [Name] -> Text
printQual ns = T.intercalate "." [m | Name m <- ns]
-- | Print an operator name. Symbolic operators are printed bare (@+@, @>>=@);
-- alphanumeric operators are wrapped in backticks (@\`div\`@, @\`elem\`@).
printOp :: Name -> Text
printOp (Name n)
| T.all isSymChar n = n
| otherwise = "`" <> n <> "`"
where
isSymChar c = c `elem` ("!#$%&*+./<=>?@\\^|-~:" :: [Char])
-- | Print an expression for the given target language.
--
-- This is the main entry point for expression printing. It dispatches
-- to the appropriate syntax based on the 'Target' and the AST node.
printExpr :: Target -> Expr -> Text
printExpr _ (Literal l) = printLit l
printExpr _ (Var (Name n)) = n
printExpr _ (Con (Name n)) = n
printExpr t (App f x) = printAppFun t f <> " " <> printAtom t x
printExpr t (InfixApp l op r) =
printInfixArg t l <> " " <> printOp op <> " " <> printInfixArg t r
printExpr t (Lam pats body) =
"\\" <> T.intercalate " " (map (printPatAtom t) pats) <> " -> " <> printExpr t body
printExpr t (If c th el) =
"if " <> printExpr t c <> " then " <> printExpr t th <> " else " <> printExpr t el
printExpr t (Case scrut alts) =
"case " <> printExpr t scrut <> " of { "
<> T.intercalate "; " (map (printCaseAlt t) alts)
<> " }"
printExpr t (Let bindings body) =
"let { " <> T.intercalate "; " (map (printBinding t) bindings)
<> " } in " <> printExpr t body
printExpr t (Do stmts) =
"do { " <> T.intercalate "; " (map (printStmt t) stmts) <> " }"
printExpr t (Neg e) = "-" <> printAppFun t e
printExpr Haskell (RecordAccess rec (Name field)) =
field <> " " <> printAtom Haskell rec
printExpr PureScript (RecordAccess rec (Name field)) =
printAtom PureScript rec <> "." <> field
printExpr Haskell (Tuple es) =
"(" <> T.intercalate ", " (map (printExpr Haskell) es) <> ")"
printExpr PureScript (Tuple [a, b]) =
"Tuple " <> printAtom PureScript a <> " " <> printAtom PureScript b
printExpr PureScript (Tuple (a : rest)) =
"Tuple " <> printAtom PureScript a <> " " <> printAtom PureScript (Tuple rest)
printExpr PureScript (Tuple []) =
error "Tuple must have at least 2 elements"
printExpr _ (ListLit []) = "[]"
printExpr t (ListLit es) = "[" <> T.intercalate ", " (map (printExpr t) es) <> "]"
printExpr t (LeftSection e op) = "(" <> printInfixArg t e <> " " <> printOp op <> ")"
printExpr t (RightSection op e) = "(" <> printOp op <> " " <> printInfixArg t e <> ")"
printExpr t (Where e bs) =
printInfixLevel t e <> " where { " <> T.intercalate "; " (map (printBinding t) bs) <> " }"
printExpr t (Ann e ty) =
printInfixLevel t e <> " :: " <> printType ty
printExpr t (RecordUpdate e fields) =
printAtom t e <> " { " <> T.intercalate ", " (map (printRecField t e) fields) <> " }"
printExpr _ (QVar qual (Name n)) = printQual qual <> "." <> n
printExpr _ (QCon qual (Name n)) = printQual qual <> "." <> n
printRecField :: Target -> Expr -> (Name, Expr) -> Text
printRecField t base (Name n, val)
| PureScript <- t, isConExpr base = n <> ": " <> printExpr t val
| otherwise = n <> " = " <> printExpr t val
isConExpr :: Expr -> Bool
isConExpr (Con _) = True
isConExpr (QCon _ _) = True
isConExpr _ = False
-- Parenthesization helpers
-- | Determine whether an expression needs parentheses in atom position.
isCompound :: Target -> Expr -> Bool
isCompound Haskell (Tuple {}) = False
isCompound PureScript (Tuple {}) = True
isCompound _ (ListLit {}) = False
isCompound _ (LeftSection {}) = False
isCompound _ (RightSection {}) = False
isCompound _ (Neg {}) = True
isCompound Haskell (RecordAccess {}) = True
isCompound PureScript (RecordAccess {}) = False
isCompound _ (App {}) = True
isCompound _ (InfixApp {}) = True
isCompound _ (Lam {}) = True
isCompound _ (If {}) = True
isCompound _ (Case {}) = True
isCompound _ (Let {}) = True
isCompound _ (Do {}) = True
isCompound _ (Where {}) = True
isCompound _ (Ann {}) = True
isCompound _ _ = False
-- | Print an expression in atom position, adding parentheses if needed.
--
-- Atoms are the tightest binding context: function arguments,
-- record update targets, etc.
printAtom :: Target -> Expr -> Text
printAtom t e
| isCompound t e = "(" <> printExpr t e <> ")"
| otherwise = printExpr t e
-- | Print an expression in function position of an application.
--
-- Like 'printAtom' but allows nested applications without parentheses
-- (since application is left-associative).
printAppFun :: Target -> Expr -> Text
printAppFun t e@(App {}) = printExpr t e -- left-assoc app doesn't need parens on left
printAppFun t e
| isCompound t e = "(" <> printExpr t e <> ")"
| otherwise = printExpr t e
-- | Print an expression as an argument to an infix operator.
--
-- Parenthesizes nested infix expressions, lambdas, and other constructs
-- that would be ambiguous.
printInfixArg :: Target -> Expr -> Text
printInfixArg t e@(InfixApp {}) = "(" <> printExpr t e <> ")"
printInfixArg t e@(Lam {}) = "(" <> printExpr t e <> ")"
printInfixArg t e@(If {}) = "(" <> printExpr t e <> ")"
printInfixArg t e@(Case {}) = "(" <> printExpr t e <> ")"
printInfixArg t e@(Let {}) = "(" <> printExpr t e <> ")"
printInfixArg t e@(Do {}) = "(" <> printExpr t e <> ")"
printInfixArg t e@(Where {}) = "(" <> printExpr t e <> ")"
printInfixArg t e@(Ann {}) = "(" <> printExpr t e <> ")"
printInfixArg t e = printExpr t e
-- | Print an expression at infix level (for where\/annotation left-hand sides).
--
-- Parenthesizes lambdas, conditionals, and other non-infix constructs
-- but allows infix expressions and applications through.
printInfixLevel :: Target -> Expr -> Text
printInfixLevel t e@(Lam {}) = "(" <> printExpr t e <> ")"
printInfixLevel t e@(If {}) = "(" <> printExpr t e <> ")"
printInfixLevel t e@(Case {}) = "(" <> printExpr t e <> ")"
printInfixLevel t e@(Let {}) = "(" <> printExpr t e <> ")"
printInfixLevel t e@(Do {}) = "(" <> printExpr t e <> ")"
printInfixLevel t e@(Where {}) = "(" <> printExpr t e <> ")"
printInfixLevel t e@(Ann {}) = "(" <> printExpr t e <> ")"
printInfixLevel t e = printExpr t e
-- Guard, case alt, binding, and statement printers
-- | Print a single guard condition.
printGuard :: Target -> Guard -> Text
printGuard t (Guard e) = "| " <> printInfixLevel t e
-- | Print a list of guards, or empty text if there are none.
printGuards :: Target -> [Guard] -> Text
printGuards _ [] = ""
printGuards t gs = " " <> T.intercalate " " (map (printGuard t) gs)
-- | Print a case alternative: @pattern -> body@ or @pattern | guard -> body@.
printCaseAlt :: Target -> CaseAlt -> Text
printCaseAlt t (CaseAlt pat guards body) =
printPat t pat <> printGuards t guards <> " -> " <> printExpr t body
-- | Print a let\/where binding.
--
-- Function-style bindings (@f x = body@) are recognized when the pattern
-- is a 'VarPat' and the body is a 'Lam', and printed in compact form
-- rather than as @f = \\x -> body@.
printBinding :: Target -> Binding -> Text
printBinding t (Binding (VarPat name) (Lam pats body)) =
printPat t (VarPat name) <> " " <> T.intercalate " " (map (printPatAtom t) pats) <> " = " <> printExpr t body
printBinding t (Binding pat body) = printPat t pat <> " = " <> printExpr t body
-- | Print a do-notation statement.
printStmt :: Target -> Stmt -> Text
printStmt t (StmtBind pat body) = printPat t pat <> " <- " <> printExpr t body
printStmt t (StmtExpr e) = printExpr t e
printStmt t (StmtLet bindings) =
"let { " <> T.intercalate "; " (map (printBinding t) bindings) <> " }"
-- Type printer
-- | Print a type expression.
printType :: Type -> Text
printType (TyCon (Name n)) = n
printType (TyVar (Name n)) = n
printType (TyApp f x) = printTyAppFun f <> " " <> printTyAtom x
printType (TyFun a b) = printTyFunArg a <> " -> " <> printType b
printType (TyQCon qual (Name n)) = printQual qual <> "." <> n
printTyAtom :: Type -> Text
printTyAtom ty@(TyApp {}) = "(" <> printType ty <> ")"
printTyAtom ty@(TyFun {}) = "(" <> printType ty <> ")"
printTyAtom ty = printType ty
printTyAppFun :: Type -> Text
printTyAppFun ty@(TyFun {}) = "(" <> printType ty <> ")"
printTyAppFun ty = printType ty
printTyFunArg :: Type -> Text
printTyFunArg ty@(TyFun {}) = "(" <> printType ty <> ")"
printTyFunArg ty = printType ty
-- Literal printer
-- | Print a literal value with proper escaping.
printLit :: Lit -> Text
printLit (IntLit n) = T.pack (show n)
printLit (FloatLit d) = T.pack (show d)
printLit (StringLit s) = "\"" <> escapeString s <> "\""
printLit (CharLit c) = "'" <> escapeChar c <> "'"
escapeString :: Text -> Text
escapeString = T.concatMap escapeStringChar
where
escapeStringChar '"' = "\\\""
escapeStringChar '\\' = "\\\\"
escapeStringChar '\n' = "\\n"
escapeStringChar '\t' = "\\t"
escapeStringChar c = T.singleton c
escapeChar :: Char -> Text
escapeChar '\'' = "\\'"
escapeChar '\\' = "\\\\"
escapeChar '\n' = "\\n"
escapeChar '\t' = "\\t"
escapeChar c = T.singleton c
-- Pattern printers
-- | Print a pattern for the given target language.
--
-- Handles divergent syntax:
--
-- * __Tuples__: @(a, b)@ (Haskell) vs @Tuple a b@ (PureScript)
-- * __Cons__: @x : xs@ (Haskell) vs @Cons x xs@ (PureScript)
-- * __Record fields__: @=@ separator (Haskell) vs @:@ separator (PureScript)
printPat :: Target -> Pat -> Text
printPat _ (VarPat (Name n)) = n
printPat _ (LitPat l) = printLit l
printPat _ WildPat = "_"
printPat _ (ConPat (Name n) []) = n
printPat t (ConPat (Name n) args) = n <> " " <> T.intercalate " " (map (printPatAtom t) args)
printPat Haskell (TuplePat ps) =
"(" <> T.intercalate ", " (map (printPat Haskell) ps) <> ")"
printPat PureScript (TuplePat [a, b]) =
"Tuple " <> printPatAtom PureScript a <> " " <> printPatAtom PureScript b
printPat PureScript (TuplePat (a : rest)) =
"Tuple " <> printPatAtom PureScript a <> " " <> printPatAtom PureScript (TuplePat rest)
printPat PureScript (TuplePat []) =
error "TuplePat must have at least 2 elements"
printPat _ (ListPat []) = "[]"
printPat t (ListPat ps) = "[" <> T.intercalate ", " (map (printPat t) ps) <> "]"
printPat Haskell (ConsPat l r) = printPatAtom Haskell l <> " : " <> printPat Haskell r
printPat PureScript (ConsPat l r) = "Cons " <> printPatAtom PureScript l <> " " <> printPatAtom PureScript r
printPat t (AsPat (Name n) p) = n <> "@" <> printPatAtom t p
printPat _ (NegLitPat l) = "-" <> printLit l
printPat t (RecordPat (Name n) fields) =
n <> " { " <> T.intercalate ", " (map pf fields) <> " }"
where pf (Name fn, p) = fn <> sep <> printPat t p
sep = case t of { PureScript -> ": "; Haskell -> " = " }
-- | Print a pattern in atom position, adding parentheses where needed.
--
-- Constructor patterns with arguments, tuple patterns (in PureScript),
-- cons patterns, as-patterns, negated literals, and record patterns
-- all require parentheses in atom position.
printPatAtom :: Target -> Pat -> Text
printPatAtom t p@(ConPat _ (_:_)) = "(" <> printPat t p <> ")"
printPatAtom t p@(TuplePat _) = case t of
Haskell -> printPat t p -- already parenthesized
PureScript -> "(" <> printPat t p <> ")"
printPatAtom t p@(ConsPat _ _) = "(" <> printPat t p <> ")"
printPatAtom t p@(AsPat _ _) = "(" <> printPat t p <> ")"
printPatAtom t p@(NegLitPat _) = "(" <> printPat t p <> ")"
printPatAtom t p@(RecordPat _ _) = "(" <> printPat t p <> ")"
printPatAtom t p = printPat t p