Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
e170fc8
feat: implement comment parser
leana8959 Sep 25, 2025
6624ebf
hide internal comment manipulating functions until API is stable
leana8959 Jun 12, 2026
47e8c45
fix lint issues
leana8959 Jun 12, 2026
c8ab7f2
Retain comments in field parser
leana8959 Jul 20, 2026
f29a92f
document types and their methods
leana8959 Jul 27, 2026
6ea1499
rewrite mapComments and mapCommentData using lens
leana8959 Jul 27, 2026
8ff46fd
remove unused instances
leana8959 Jul 27, 2026
968eace
remove strict either
leana8959 Jul 27, 2026
47570de
run forumolu
leana8959 Jul 27, 2026
18e8202
remove use of Functor in commentsAfter
leana8959 Jul 28, 2026
6463836
don't expose cabal-parsec-debug flag
leana8959 Jul 29, 2026
c599c87
fix pragma in testsuite
leana8959 Aug 11, 2026
1e24ac2
clean up pragmas, reduce diff, reword wonky comments
leana8959 Aug 14, 2026
9993557
add ascii flowchart of the lexer
leana8959 Aug 17, 2026
e0d67b9
implement basic idea of field transformation
leana8959 Jul 1, 2026
5c56bf9
Merge branch 'exact-pp-leana' into transform-fields
leana8959 Aug 17, 2026
a65156d
replace Newtype class with Coerce class
leana8959 Aug 17, 2026
c3dafbd
fix unterminated pragma
leana8959 Aug 17, 2026
b21a4da
fix newtype class in parser tests
leana8959 Aug 17, 2026
28902d8
add a pass to turn name to lower case
leana8959 Aug 17, 2026
b3e55f1
accept casing in concrete parsing
leana8959 Aug 17, 2026
3c8c107
document the reason of orFallback
leana8959 Aug 17, 2026
f8d5fd0
add comment handling in modification functions
leana8959 Aug 17, 2026
777b11a
update comment handling in test suite
leana8959 Aug 17, 2026
e76eb1e
add comment in example
leana8959 Aug 17, 2026
ccda102
add export list to transform module
leana8959 Aug 18, 2026
04742a0
make modifyValueList partial
leana8959 Aug 18, 2026
10772d5
make modifyValueAtomAla partial
leana8959 Aug 18, 2026
dbbffad
allow using partial modification function in modify focuses
leana8959 Aug 18, 2026
de0d96b
add instances to fix test
leana8959 Aug 18, 2026
81aeb3e
update todos
leana8959 Aug 18, 2026
af418ce
remove project files
leana8959 Aug 18, 2026
855be0c
add todos
leana8959 Aug 18, 2026
4ea6ff7
add export list for ExactPretty
leana8959 Aug 18, 2026
9a6011d
add proper position shifting logic in new field
leana8959 Aug 18, 2026
5e3f24d
fix colonPos calculation
leana8959 Aug 18, 2026
6974966
remove todo
leana8959 Aug 18, 2026
43ad1b3
add a basic add section function
leana8959 Aug 18, 2026
6b0974d
make prependValueListBS partial
leana8959 Aug 18, 2026
c1e2828
add a function to prepend function list
leana8959 Aug 18, 2026
bae4047
move code around
leana8959 Aug 18, 2026
8e724e2
clean up imports
leana8959 Aug 18, 2026
c80115c
implement example formatter to test out the idea
leana8959 Aug 20, 2026
cae82ce
add a function to run the formatter
leana8959 Aug 20, 2026
30ae685
run the build-depends formatter as an example
leana8959 Aug 20, 2026
635d9e1
add shared context to edit functions
leana8959 Aug 20, 2026
9b493f2
move code around, remove dead code
leana8959 Aug 20, 2026
4f02cf8
implement sectionArgBS
leana8959 Aug 20, 2026
5b1e003
refactor using monad instance
leana8959 Aug 20, 2026
e510c6c
run formatter
leana8959 Aug 20, 2026
9ec34ae
liftA2 is cool
leana8959 Aug 20, 2026
617f14d
move parsec orphan instance to its module
leana8959 Aug 20, 2026
ee8d9d3
update todo on labels
leana8959 Aug 20, 2026
4ee6831
reuse transform functions to format
leana8959 Aug 20, 2026
684ee41
proper formatting example with multiple sections
leana8959 Aug 20, 2026
25cd0eb
add modifyConfitionConfVar
leana8959 Aug 26, 2026
9ed5478
document position handling
leana8959 Aug 26, 2026
566eeb9
run formatter
leana8959 Aug 26, 2026
0777da7
fix test suite
leana8959 Sep 4, 2026
0aac25c
add fields to example
leana8959 Sep 4, 2026
df5d3fe
demonstrate printing
leana8959 Sep 4, 2026
73a19d9
rip out formatter poc
leana8959 Sep 4, 2026
f9a52ad
clean up warnings
leana8959 Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cabal-syntax/Cabal-syntax.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ build-type: Simple
extra-doc-files:
README.md ChangeLog.md

flag cabal-parsec-debug
description: Enable debug build for the cabal field lexer/parser.
default: False
manual: True

source-repository head
type: git
location: https://github.com/haskell/cabal/
Expand Down Expand Up @@ -60,11 +65,17 @@ library
if impl(ghc >= 8.0) && impl(ghc < 8.8)
ghc-options: -Wnoncanonical-monadfail-instances

if flag(cabal-parsec-debug)
CPP-Options: -DCABAL_PARSEC_DEBUG
build-depends:
vector

build-tool-depends:
-- https://github.com/haskell/alex/issues/288
alex:alex < 3.5.4.1 || > 3.5.4.1

exposed-modules:
Distribution.Annotation
Distribution.Backpack
Distribution.CabalSpecVersion
Distribution.Compat.Binary
Expand All @@ -86,11 +97,14 @@ library
Distribution.Fields
Distribution.Fields.ConfVar
Distribution.Fields.Field
Distribution.Fields.Field.Lens
Distribution.Fields.Lexer
Distribution.Fields.LexerMonad
Distribution.Fields.Transform
Distribution.Fields.ParseResult
Distribution.Fields.Parser
Distribution.Fields.Pretty
Distribution.Fields.ExactPretty
Distribution.InstalledPackageInfo
Distribution.License
Distribution.ModuleName
Expand All @@ -106,6 +120,7 @@ library
Distribution.Parsec.Error
Distribution.Parsec.FieldLineStream
Distribution.Parsec.Position
Distribution.Parsec.Position.Lens
Distribution.Parsec.Warning
Distribution.Parsec.Source
Distribution.Pretty
Expand Down
30 changes: 30 additions & 0 deletions Cabal-syntax/src/Distribution/Annotation.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{-# LANGUAGE DeriveFunctor #-}

Check warning on line 1 in Cabal-syntax/src/Distribution/Annotation.hs

View workflow job for this annotation

GitHub Actions / hlint

Warning in module Distribution.Annotation: Unused LANGUAGE pragma ▫︎ Found: "{-# LANGUAGE DeriveFunctor #-}" ▫︎ Note: Extension DeriveFunctor is implied by DeriveTraversable
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE UndecidableInstances #-}

module Distribution.Annotation where

import Data.List (sortOn)
import Data.Ord (Down (..))
import Distribution.Parsec

-- | Designates the source position within a joined 'FieldLineStream'.
-- Inclusive in the start position, exclusive in the ending position.
data SrcSpan = SrcSpan {-# UNPACK #-} !Position {-# UNPACK #-} !Position
deriving (Show, Eq, Ord)

data Located a = MkLocated {getSrcSpan :: !SrcSpan, unLocated :: !a}
deriving (Show, Functor, Foldable, Traversable)

sortBySrcSpanAsc :: [Located a] -> [Located a]
sortBySrcSpanAsc = sortOn getSrcSpan

sortBySrcSpanDes :: [Located a] -> [Located a]
sortBySrcSpanDes = sortOn (Down . getSrcSpan)

instance Parsec a => Parsec (Located a) where
parsec = do
begin <- getPosition
x <- parsec
end <- getPosition
pure (MkLocated (SrcSpan begin end) x)
5 changes: 3 additions & 2 deletions Cabal-syntax/src/Distribution/FieldGrammar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ partitionFields = finalize . foldl' f (PS mempty mempty mempty)
| otherwise = (fs, reverse (reverse s : ss))

f :: PS ann -> Field ann -> PS ann
f (PS fs s ss) (Field (Name ann name) fss) =
f (PS fs s ss) (Field _ (Name ann name) fss) =
PS (Map.insertWith (flip (++)) name [MkNamelessField ann fss] fs) [] ss'
where
ss'
Expand All @@ -95,10 +95,11 @@ partitionFields = finalize . foldl' f (PS mempty mempty mempty)
PS fs (MkSection name sargs sfields : s) ss

-- | Take all fields from the front.
-- Returns a tuple containing the comments, nameless fields, and sections
takeFields :: [Field ann] -> (Fields ann, [Field ann])
takeFields = finalize . spanMaybe match
where
finalize (fs, rest) = (Map.fromListWith (flip (++)) fs, rest)

match (Field (Name ann name) fs) = Just (name, [MkNamelessField ann fs])
match (Field _ (Name ann name) fs) = Just (name, [MkNamelessField ann fs])
match _ = Nothing
22 changes: 22 additions & 0 deletions Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import Distribution.Version
)
import Text.PrettyPrint (Doc, comma, fsep, punctuate, text, vcat)

import qualified Data.ByteString as BS
import Data.Coerce (Coercible, coerce)
import qualified Data.List.NonEmpty as NE
import qualified Data.Set as Set
Expand All @@ -88,12 +89,21 @@ data FSep = FSep
data NoCommaFSep = NoCommaFSep

class Sep sep where
-- | Detect whether a string is considered valid separator.
-- Used in exactprint modification.
isSeparator :: Proxy sep -> Char -> Bool

sepToChar :: Proxy sep -> BS.ByteString

prettySep :: Proxy sep -> [Doc] -> Doc

parseSep :: CabalParsing m => Proxy sep -> m a -> m [a]
parseSepNE :: CabalParsing m => Proxy sep -> m a -> m (NonEmpty a)

instance Sep CommaVCat where
isSeparator _ = (== ',')
sepToChar _ = ",\n"

prettySep _ = vcat . punctuate comma
parseSep _ p = do
v <- askCabalSpecVersion
Expand All @@ -102,6 +112,9 @@ instance Sep CommaVCat where
v <- askCabalSpecVersion
if v >= CabalSpecV2_2 then parsecLeadingCommaNonEmpty p else parsecCommaNonEmpty p
instance Sep CommaFSep where
isSeparator _ = (== ',')
sepToChar _ = ","

prettySep _ = fsep . punctuate comma
parseSep _ p = do
v <- askCabalSpecVersion
Expand All @@ -110,18 +123,27 @@ instance Sep CommaFSep where
v <- askCabalSpecVersion
if v >= CabalSpecV2_2 then parsecLeadingCommaNonEmpty p else parsecCommaNonEmpty p
instance Sep VCat where
isSeparator _ c = c == ',' || isSpace c
sepToChar _ = "\n"

prettySep _ = vcat
parseSep _ p = do
v <- askCabalSpecVersion
if v >= CabalSpecV3_0 then parsecLeadingOptCommaList p else parsecOptCommaList p
parseSepNE _ p = NE.some1 (p <* P.spaces)
instance Sep FSep where
isSeparator _ c = c == ',' || isSpace c
sepToChar _ = " "

prettySep _ = fsep
parseSep _ p = do
v <- askCabalSpecVersion
if v >= CabalSpecV3_0 then parsecLeadingOptCommaList p else parsecOptCommaList p
parseSepNE _ p = NE.some1 (p <* P.spaces)
instance Sep NoCommaFSep where
isSeparator _ = isSpace
sepToChar _ = " "

prettySep _ = fsep
parseSep _ p = many (p <* P.spaces)
parseSepNE _ p = NE.some1 (p <* P.spaces)
Expand Down
70 changes: 70 additions & 0 deletions Cabal-syntax/src/Distribution/FieldGrammar/Parsec.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}

Check warning on line 4 in Cabal-syntax/src/Distribution/FieldGrammar/Parsec.hs

View workflow job for this annotation

GitHub Actions / hlint

Warning in module Distribution.FieldGrammar.Parsec: Unused LANGUAGE pragma ▫︎ Found: "{-# LANGUAGE ViewPatterns #-}"

-- | This module provides a 'FieldGrammarParser', one way to parse
-- @.cabal@ -like files.
Expand Down Expand Up @@ -63,6 +66,11 @@
, runFieldParser'
, fieldLinesToStream
, freeTextIgnoreDotlineVers
, joinFieldLines
, splitFieldLines
, extractComments
, removeComments
, interleaveComments
) where

import Distribution.Compat.Prelude
Expand All @@ -71,10 +79,14 @@
import Prelude ()

import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import Data.Coerce (Coercible, coerce)
import Data.Foldable1
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Distribution.Compat.Lens as L
import qualified Distribution.Parsec.Position.Lens as L
import qualified Distribution.Utils.ShortText as ShortText
import qualified Text.Parsec as P
import qualified Text.Parsec.Error as P
Expand Down Expand Up @@ -495,3 +507,61 @@
fieldLinesToStream [] = fieldLineStreamEnd
fieldLinesToStream [FieldLine _ bs] = FLSLast bs
fieldLinesToStream (FieldLine _ bs : fs) = FLSCons bs (fieldLinesToStream fs)

-- | Take all comments out from a structure
extractComments :: Foldable t => t (WithComments ann) -> [Comment ann]
extractComments = foldMap justComments

-- | Remove all comments from a structure
removeComments :: Functor f => f (WithComments ann) -> f ann
removeComments = fmap unComments

-- | Biased to put comments as trailing.
-- Precondition: both list are sorted by 'Position' (asc).
interleaveComments :: [FieldLine Position] -> [Comment Position] -> [FieldLine (WithComments Position)]
interleaveComments [] _ = [] -- We have nothing to attach the comment to, considered it deleted.
interleaveComments [FieldLine pos bs] cmts = [FieldLine (WithComments cmts pos) bs]
interleaveComments (FieldLine pos bs : fls) cmts =
let (pre, post) = span (\(Comment _ cpos) -> pos < cpos) cmts
in FieldLine (WithComments pre pos) bs : interleaveComments fls post

-- TODO(leana8959): add property tests

joinFieldLines :: NonEmpty (FieldLine Position) -> FieldLine Position
-- No indentation needed
joinFieldLines (FieldLine ann bs :| []) = FieldLine ann bs
-- Fixup missing whitespaces, then join
joinFieldLines fsNE =
let leftmostCol = foldl1' min $ fmap (L.view L.positionCol . fieldLineAnn) fsNE
ann0 = fieldLineAnn (NE.head fsNE)
indented = map (indentFieldLine leftmostCol) (NE.toList fsNE)
bss = toBSWithNewlines (L.view L.positionRow ann0) indented
in FieldLine (L.set L.positionCol leftmostCol ann0) bss

-- | Indent a FieldLine while preserving 'FieldLine' invariants.
indentFieldLine :: L.HasPosition ann => Int -> FieldLine ann -> FieldLine ann
indentFieldLine leftmostCol (FieldLine ann bs) =
let myCol = L.view L.positionCol ann
indent = myCol - leftmostCol
in FieldLine (L.set L.positionCol leftmostCol ann) (BS8.replicate indent ' ' <> bs)

toBSWithNewlines :: L.HasPosition ann => Int -> [FieldLine ann] -> BS.ByteString
toBSWithNewlines row0 = mconcat . mealy go row0
where
go row (FieldLine ann bs) =
let myRow = L.view L.positionRow ann
newlines = myRow - row
in (myRow, BS8.replicate newlines '\n' <> bs)

-- | Lines the inner 'ByteString', remove empty lines, distributing start colomn numbers and enumerate row numbers.
-- We assume that the joined field lines have been aligned to the same column.
splitFieldLines :: FieldLine Position -> [FieldLine Position]
splitFieldLines (FieldLine ann bs0) =
let ls = BS8.lines bs0
Position startRow startCol = L.view L.position ann
rowNs = [startRow ..]
in zipWith
( \bs row -> FieldLine (Position row startCol) bs
)
ls
rowNs
54 changes: 52 additions & 2 deletions Cabal-syntax/src/Distribution/Fields/ConfVar.hs
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Distribution.Fields.ConfVar (parseConditionConfVar, parseConditionConfVarFromClause) where
module Distribution.Fields.ConfVar
( -- * Parsers
parseConditionConfVar
, parseConditionConfVarFromClause
, confVarParser

-- * Printers
, ppConfVar
, ppFlagName
, ppCondition
)
where

import Data.Functor ((<&>))
import Distribution.Compat.CharParsing (char, integral)
import Distribution.Compat.Prelude
import Distribution.Fields.Field (Field (..), SectionArg (..), sectionArgAnn)
import Distribution.Fields.ParseResult
import Distribution.Fields.Parser (readFields)
import Distribution.Parsec (Parsec (..), runParsecParser)
import Distribution.Parsec.FieldLineStream (fieldLineStreamFromBS)
import Distribution.Parsec.Position
import Distribution.Types.Condition
import Distribution.Types.ConfVar (ConfVar (..))
import Distribution.Types.Flag
import Distribution.Version
( anyVersion
, earlierVersion
Expand All @@ -31,10 +42,15 @@ import Distribution.Version
import Prelude ()

import qualified Data.ByteString.Char8 as B8
import Distribution.Parsec
import qualified Text.Parsec as P
import qualified Text.Parsec.Error as P
import qualified Text.Parsec.Pos as P

import Distribution.Pretty
import Text.PrettyPrint (Doc)
import qualified Text.PrettyPrint as PP

parseConditionConfVarFromClause :: B8.ByteString -> Either P.ParseError (Condition ConfVar)
parseConditionConfVarFromClause x =
readFields x >>= \case
Expand Down Expand Up @@ -71,6 +87,40 @@ type Parser = P.Parsec [SectionArg Position] ()
sepByNonEmpty :: Parser a -> Parser sep -> Parser (NonEmpty a)
sepByNonEmpty p sep = (:|) <$> p <*> many (sep *> p)

-- TODO(leana8959): make the parser and pretty printer a class
confVarParser :: Parser (Condition ConfVar)
confVarParser = parser

ppCondition :: Condition ConfVar -> Doc
ppCondition (Var x) = ppConfVar x
ppCondition (Lit b) = PP.text (show b)
ppCondition (CNot c) = PP.char '!' <<>> ppCondition c
ppCondition (COr c1 c2) =
PP.parens
( PP.hsep
[ ppCondition c1
, PP.text "||"
<+> ppCondition c2
]
)
ppCondition (CAnd c1 c2) =
PP.parens
( PP.hsep
[ ppCondition c1
, PP.text "&&"
<+> ppCondition c2
]
)

ppConfVar :: ConfVar -> Doc
ppConfVar (OS os) = PP.text "os" <<>> PP.parens (pretty os)
ppConfVar (Arch arch) = PP.text "arch" <<>> PP.parens (pretty arch)
ppConfVar (PackageFlag name) = PP.text "flag" <<>> PP.parens (ppFlagName name)
ppConfVar (Impl c v) = PP.text "impl" <<>> PP.parens (pretty c <+> pretty v)

ppFlagName :: FlagName -> Doc
ppFlagName = PP.text . unFlagName

parser :: Parser (Condition ConfVar)
parser = condOr
where
Expand Down
Loading
Loading