TTG: Resolving even more TTG-related orphan type-class instances

TTG: Resolving orphan type-class instances after AST separation

Introduction

The Trees that Grow (TTG) migration relocated the Haskell abstract syntax tree (AST) into the Language.Haskell.Syntax (L.H.S) namespace and left GHC's instantiation of it in the GHC namespace. While the data types moved, their instances largely did not. The result is a population of orphan type-class instances, declared in modules that define neither the class nor the type. These modules contain a -Wno-orphans pragma which hides/suppresses technical debt.

Now that the two namespaces have been partitioned, it is possible to resolve many of the orphan type-class instances that remain as either an artifact or temporary kludge of the TTG project. This merge request retires the movable portion of that debt. It changes no AST semantics: every migrated body preserves its original encoding, pretty-printed output and ordering behavior.

Addresses issue #27755

Scope of Work

Orphan counts are measured on master from .hie interface files for modules with a -Wno-orphans pragma. Prior to this MR, master carries 663 orphan class instances across 53 suppressing modules:

Universe of Orphan Instances

Type-class On master Within TTG-decoupled modules
Binary 37 16
BooleanFormulaDefault 1 1
Data 425 9
Diagnostic 6 0
Eq 35 24
Fractional 1 0
HasDefaultDiagnosticOpts 4 0
HasLoc 5 5
HasOccName 1 1
Instruction 5 0
MonadUnique 1 0
NFData 1 1
Num 1 0
Ord 6 6
Outputable 120 98
OutputableBndr 9 6
Show 4 4
WrapXRec 1 1
Total 663 172

Several categories of orphan type-class instances are set aside before any migration is considered:

This leaves 9 type-classes totaling 133 instances “in scope” as technical debt from the TTG project.

Candidate Orphan Instances

Type-class On master
Binary 16
BooleanFormulaDefault 1
HasLoc 5
HasOccName 1
NFData 1
Outputable 98
OutputableBndr 6
Show 4
WrapXRec 1
Total 133

Methodology

Seven techniques were applied. The first is the default. the remainder exist because a verbatim migration fails.

  1. Migration to type-class module verbatim. Move the instance directly into the module declaring the class. Applies where the body is already independent of the pass index.

  2. Migration to data-type module. Where the class is in base or deepseq, or its module is cyclically unreachable from the class module, the data-type's module in L.H.S is the only admissible home.

  3. Constraint generalization over the pass. Restate the instance at an arbitrary index p and replace each GhcPass type-family equation the body relied on with a constraint. Two forms recur: an XXFoo p ~ equality against DataConCantHappen, required for the pattern-match coverage checker to prove the extension constructor unreachable, and UnXRec p in place of a direct unLoc. This is what allows a printer or serializer to live in the class module without that module gaining any knowledge of GhcPass.

  4. Dispatch elimination. A case ghcPass @p of … whose branches are identical, or which exists only so that GHC can reduce a family and find an instance, is a symptom of a missing payload instance rather than a genuine obstacle. Replacing it with a constraint on the payload — and declaring the payload's instance in the payload's own module, where it is a non-orphan — removes the IsPass requirement instead of satisfying it.

  5. Dependency inversion. Where a module's only obstacle is that it imports the class module, invert the edge: move its instances out to the class module and reduce it to a leaf. Applied to GHC.Types.SourceText and GHC.Utils.Lexeme.

  6. Shared-accessor lifting. Where two candidate homes each need the same pure accessor, define it once in L.H.S and have both use it. ieWrappedLIdP and the HsDocString render chain are the two cases.

  7. Referential-transparency expansion. In order to relocate pprPrefixFastString to GHC.Utils.Outputable, the helper's definition was expanded by substitution, reduced the closed sub-expressions, and repeated until the residual expression contains only symbols GHC.Utils.Outputable can import. Eleven substitution steps reduce pprPrefixOcc . mkVarUnqual to a form whose only external needs are isLexConSym/isLexVarSym.

    Click to expand referential transparency proof
    {-
    pprPrefixFastString = pprPrefixOcc . mkVarUnqual
                        = pprPrefixOcc . (\n ->  Unqual (mkVarOccFS n))
                        = pprPrefixOcc . Unqual . mkVarOccFS
                        = pprPrefixOcc . Unqual . (\fs -> mkOccNameFS VarName fs)
                        = pprPrefixOcc . Unqual . mkOccNameFS VarName
                        = pprPrefixOcc . Unqual . ((\occ_sp fs ->  OccName occ_sp fs) VarName)
                        = pprPrefixOcc . Unqual . OccName VarName
                        = \fs -> pprPrefixOcc (Unqual (OccName (VarName fs)))
                        = \fs -> (\rdr -> pprPrefixVar (isSymOcc (rdrNameOcc rdr)) (ppr rdr)) (Unqual (OccName VarName fs))
                        = \fs -> pprPrefixVar (isSymOcc (rdrNameOcc (Unqual (OccName VarName fs))) (ppr (Unqual (OccName VarName fs))))
                        = \fs -> pprPrefixVar (isSymOcc ((OccName VarName fs)) (ppr (Unqual (OccName VarName fs))))
                        = \fs -> pprPrefixVar (\(OccName VarName fs) -> isLexSym fs) (ppr (Unqual (OccName VarName fs))))
                        = \fs -> pprPrefixVar (isLexSym fs) (ppr (Unqual (OccName VarName fs))))
                        = \fs -> pprPrefixVar (isLexConSym fs || isLexVarSym fs) (ppr (Unqual (OccName VarName fs))))
                        = \fs -> pprPrefixVar (isLexConSym fs || isLexVarSym fs) (\(Unqual (OccName VarName fs)) -> ppr (OccName VarName fs))
                        = \fs -> pprPrefixVar (isLexConSym fs || isLexVarSym fs) (ppr (OccName VarName fs))
                        = \fs -> pprPrefixVar (isLexConSym fs || isLexVarSym fs) (pprOccName (OccName VarName fs))
                        = \fs -> pprPrefixVar (isLexConSym fs || isLexVarSym fs)
                                              (docWithStyle (ztext (zEncodeFS fs)) (\_ -> ftext fs <> whenPprDebug (braces (pprNameSpaceBrief VarName))))
                        = \fs -> pprPrefixVar (isLexConSym fs || isLexVarSym fs)
                                              (docWithStyle (ztext (zEncodeFS fs)) (\_ -> ftext fs <> whenPprDebug (braces (char 'v'))))
    -}
    pprPrefixFastString :: FastString -> SDoc
    pprPrefixFastString = \fs -> pprPrefixVar (Lex.isLexConSym fs || Lex.isLexVarSym fs)
                                              (docWithStyle (ztext (zEncodeFS fs)) (\_ -> ftext fs <> whenPprDebug (braces (char 'v'))))

Architectural Changes

A new module, GHC.Hs.Extension.Instances, now holds the GhcPass instantiation of the TTG extension points that were previously declared in GHC.Hs.Extension.Pass — the XRec/Anno equations, the HsDocString extension points, and the UnXRec/MapXRec instances.

The effect is to concentrate the orphans that necessarily accompany those equations into one module in order to avoid module import cycles. Now GHC.Hs.Extension.Pass can be imported by other modules without transitively depending on modules required for the instances. GHC.Hs.Extension.Pass also gains the IsPass class and the NoGhcTcPass family, moved down from GHC.Hs.Extension with no new imports, on the grounds that both are about GhcPass.

Conclusion

82 of the 133 in-scope orphan class instances are resolved!

Results

Type-class Before After
Binary 16 1
BooleanFormulaDefault † 1 1
HasLoc 5 1
HasOccName 1 0
NFData 1 0
Outputable 98 47
OutputableBndr 6 0
Show 4 0
WrapXRec † 1 1
Total 133 51

† See the Other migrations section below.

This constitutes a 61.7% reduction in TTG-related orphan type-class instances (82 of 133). The two daggered rows are migrated but not un-orphaned, so they count against the total rather than towards it.

Resolved Orphan Instances

The table below aggregates all 82 resolved orphan type-class instances.

Click to expand table of resolved orphan type-class instances (all instances)
Data-type Resolved instance(s) Former orphan module(s)
BCOByteArray Binary GHC.ByteCode.Binary
BCONPtr Binary GHC.ByteCode.Binary
BCOPtr Binary GHC.ByteCode.Binary
ByteCodeHpcInfo Binary GHC.ByteCode.Binary
CompiledByteCode Binary GHC.ByteCode.Binary
UnlinkedBCO Binary GHC.ByteCode.Binary
BooleanFormula Outputable GHC.Data.BooleanFormula
HsLocalBindsLR Outputable GHC.Hs.Binds
RecordPatSynField Outputable GHC.Hs.Binds
ValBind HasLoc GHC.Hs.Binds
DerivClauseTys Outputable GHC.Hs.Decls
DocDecl Outputable GHC.Hs.Decls
FamilyInfo Outputable GHC.Hs.Decls
ForeignExport Outputable GHC.Hs.Decls
FunDep Outputable GHC.Hs.Decls
HsDecl Outputable GHC.Hs.Decls
InstDecl Outputable GHC.Hs.Decls
RoleAnnotDecl Outputable GHC.Hs.Decls
SpliceDecoration Outputable GHC.Hs.Decls
StandaloneKindSig Outputable GHC.Hs.Decls
TyClGroup Outputable GHC.Hs.Decls
WarnDecl Outputable GHC.Hs.Decls
HsDocString Binary, Outputable, Show GHC.Hs.DocString
ArithSeqInfo Outputable GHC.Hs.Expr
DotFieldOcc Outputable GHC.Hs.Expr
FieldLabelStrings Outputable, OutputableBndr GHC.Hs.Expr
HsArrowMatchContext Outputable GHC.Hs.Expr
HsLamVariant Outputable GHC.Hs.Expr
HsMatchContext Outputable GHC.Hs.Expr
Located OutputableBndr GHC.Hs.Expr
ParStmtBlock Outputable GHC.Hs.Expr
DataConCantHappen Outputable GHC.Hs.Extension
NoExtField Outputable GHC.Hs.Extension
IEWrappedName Outputable, OutputableBndr, HasOccName GHC.Hs.ImpExp
FractionalLit Outputable, Show GHC.Hs.Lit
HsLit Outputable GHC.Hs.Lit
HsQualLit Outputable GHC.Hs.Lit
IntegralLit Outputable, Show GHC.Hs.Lit
OverLitVal Outputable GHC.Hs.Lit
QualLitVal Outputable GHC.Hs.Lit
StringLiteral Outputable, Show GHC.Hs.Lit
HsFieldBind Outputable GHC.Hs.Pat
FieldOcc Outputable, OutputableBndr GHC.Hs.Type
GenLocated OutputableBndr GHC.Hs.Type
HsBndrVar Outputable GHC.Hs.Type
HsConDetails Outputable GHC.Hs.Type
HsContextDetails Outputable GHC.Hs.Type
HsForAllTelescope Outputable GHC.Hs.Type
HsIPName Outputable, OutputableBndr GHC.Hs.Type
HsPatSigType Outputable GHC.Hs.Type
HsTyPat Outputable GHC.Hs.Type
HsWildCardBndrs Outputable GHC.Hs.Type
LHsQTyVars Outputable GHC.Hs.Type
HsArg Outputable, HasLoc GHC.Hs.Type, GHC.Iface.Ext.Ast
DataDefnCons HasLoc GHC.Iface.Ext.Ast
HsDataDefn HasLoc GHC.Iface.Ext.Ast
CCallConv Binary, Outputable GHC.Types.ForeignCall
CCallTarget Binary GHC.Types.ForeignCall
CExportSpec Binary, Outputable GHC.Types.ForeignCall
CType Binary GHC.Types.ForeignCall
ForeignKind Binary GHC.Types.ForeignCall
Header Binary, Outputable GHC.Types.ForeignCall
Safety Binary, Outputable GHC.Types.ForeignCall
FieldLabel NFData GHC.Types.Name
InWarningCategory Outputable GHC.Unit.Module.Warnings
WarningCategory Binary, Outputable GHC.Unit.Module.Warnings

Remaining Orphan Instances

The 51 in-scope instances below remain. They are not arbitrary residue: each was searched individually against both admissible homes, including by the substitution procedure of technique (8), and each is blocked by one of the causes below.

  • A — AST printer layer. The body delegates to pretty-printing logic in GHC.Hs.* (pprExpr, pprPat, ppr_sig, ppr_monobind, pprConDecl and similar) and also the instance is constrained by OutputableBndrId. Since OutputableBndrId is a constraint synonym involving IdGhcP which GHC.Utils.Outputable cannot import. Without moving the vast amount of the printer definitions into GHC.Utils.Outputable (or an importable) module, or further reorganizing module structure so that OutputableBndrId can be imported by GHC.Utils.Outputable, these orphan instances cannot be resolved. An attempt at resolving these instances is far beyond the request of this MR.

  • B — GHC-side payload or annotation. The instance requires a concrete type from GHC.Parser.Annotation, GHC.Types.SrcLoc, or a GHC-specific extension payload; none of which the type-class module can import.

  • † — Relocated, not resolved. No module both declares a name in the instance head and can express the body, so the instance was moved to the most defensible home available instead. See the Other migrations section below.

Click to expand table of remaining in-scope orphan type-class instances
Data-type Remaining instance(s) Current orphan module(s) Blocker
HsModule Outputable GHC.Hs B — specialised to GhcPs; reads the XModulePs payload
HsBindLR Outputable GHC.Hs.Binds A
HsIPBinds Outputable GHC.Hs.Binds A
HsValBindsLR Outputable GHC.Hs.Binds A
IPBind Outputable GHC.Hs.Binds A
PatSynBind Outputable GHC.Hs.Binds A
Sig Outputable GHC.Hs.Binds A
AnnDecl Outputable GHC.Hs.Decls A
ClsInstDecl Outputable GHC.Hs.Decls A
ConDecl Outputable GHC.Hs.Decls A
DataFamInstDecl Outputable GHC.Hs.Decls A
DefaultDecl Outputable GHC.Hs.Decls A
DerivDecl Outputable GHC.Hs.Decls A
DerivStrategy Outputable GHC.Hs.Decls A
FamilyDecl Outputable GHC.Hs.Decls A
ForeignDecl Outputable GHC.Hs.Decls A
ForeignImport Outputable GHC.Hs.Decls B — reads staticTargetLabel/header
HsDataDefn Outputable GHC.Hs.Decls A
HsDerivingClause Outputable GHC.Hs.Decls A
HsGroup Outputable GHC.Hs.Decls A
RuleDecl Outputable GHC.Hs.Decls A
RuleDecls Outputable GHC.Hs.Decls A
SpliceDecl Outputable GHC.Hs.Decls A
TyClDecl Outputable GHC.Hs.Decls A
TyFamInstDecl Outputable GHC.Hs.Decls A
WarnDecls Outputable GHC.Hs.Decls A
WithHsDocIdentifiers Binary GHC.Hs.Doc B — needs stable Name ordering and location helpers
HsCmd Outputable GHC.Hs.Expr A
HsCmdTop Outputable GHC.Hs.Expr A
HsExpr Outputable GHC.Hs.Expr A
HsPragE Outputable GHC.Hs.Expr B — needs AnnPragma
HsQuote Outputable GHC.Hs.Expr A
HsStmtContext Outputable GHC.Hs.Expr A
Match Outputable GHC.Hs.Expr A
StmtLR Outputable GHC.Hs.Expr B — needs the concrete annotation SrcSpanAnnA
GhcPass WrapXRec GHC.Hs.Extension † — see Other migrations
GhcPass BooleanFormulaDefault GHC.Hs.Extension.Instances † — see Other migrations
IE Outputable GHC.Hs.ImpExp A
ImportDecl Outputable GHC.Hs.ImpExp B — reads XImportDeclPass/ideclSourceText
HsOverLit Outputable GHC.Hs.Lit A
HsRecFields Outputable GHC.Hs.Pat A
Pat Outputable GHC.Hs.Pat A
HsConDeclRecField Outputable GHC.Hs.Type A
HsOuterTyVarBndrs Outputable GHC.Hs.Type A
HsSigType Outputable GHC.Hs.Type A
HsTyVarBndr Outputable GHC.Hs.Type A
HsType Outputable GHC.Hs.Type A
FamEqn HasLoc GHC.Iface.Ext.Ast B — HasLoc needs HiePass, declared in GHC.Iface.Ext.Ast
CType Outputable GHC.Types.ForeignCall B — reads the CTypeGhc payload via partial field accessors
InlinePragma Outputable GHC.Types.InlinePragma A — ghcPass dispatch over module-local pragma helpers
WarningTxt Outputable GHC.Unit.Module.Warnings B — pp_ws needs LocatedA

Of the unresolved type-class instances, 40 / 51 of them are blocked by Cause A. If these are to be resolved, they should be addressed in a separate MR, though at this time I cannot present what the best best way to resolve this blocker would be, nor can I provided a sketch of what such an MR would look like. The 9 / 51 orphans blocked by Cause B will likely each need a bespoke solution if resolving those orphan instances is pursued.

Other migrations

  1. The WrapXRec (GhcPass p) orphan instance has been moved from GHC.Hs.Expr to GHC.Hs.Extension. While this is still an orphan instance, it is closer to the “logical” definition of the GhcPass data-type, and if the GHC.Hs.Extension is thought of as the “user facing API” then the instance is no longer orphaned under that viewpoint.

  2. The BooleanFormulaDefault (GhcPass p) orphan instance has been moved from GHC.Data.BooleanFormula to GHC.Hs.Extension.Instances. As with WrapXRec (GhcPass p) above, this does not remove the orphan instance, but it does place the instance beside the other GhcPass instantiations of TTG extension points, next to the XRec/Anno equations and the UnXRec/MapXRec instances, in the module whose -Wno-orphans pragma exists precisely to cover them. The same "user-facing API" viewpoint postulated for WrapXRec (GhcPass p) applied here.

A.I. Usage disclosure

An LLM was used to ensure consistent Markdown formatting, collate type-class instance tables, and add Haddock hyperlinks throughout the description of this MR. I have carefully reviewed and revised the submitted material and take responsibility for it.

Edited by recursion-ninja

Merge request reports

Loading