🌐 ν•œκ΅­μ–΄λ‘œ 읽기

07 β€” The self-hosting push

Where we are

Chapter 06 closes at 41d6a7e8, 2023-06-07 14:13:56 β€” the runtime's dedicated CHAR tag gone, the module system Haskell-shaped (qualified imports, as-aliases, required export lists), the surface syntax carrying if, operator sections, and list comprehensions. As that chapter's own "Why it matters" put it, none of it was speculative: it was aimed at "getting this compiler to eventually compile a real Haskell program, and eventually, itself, once it's written in Haskell rather than in the tiny dialect the runtime speaks." Chapter 06 also disclosed a stowaway inside a commit titled like pure housekeeping: 2f89971e ("Move files.") smuggled in src/MicroHs/Translate.hs, a first, unfinished draft of a translator from the compiler's own Exp combinator terms into native GHC values via unsafeCoerce β€” frozen mid-keystroke, an unterminated string literal on its last line, not imported by anything yet.

Then the log goes quiet for 24 days. Nothing in this repository's history explains the gap (see 00-overview.md's own timeline table); the next commit after 41d6a7e8 is 44004020 ("Small fixes."), 2023-07-01 15:41:40, followed six minutes later by a merge commit, d128fb6f, reconciling a stale branch that had fallen out of sync back on 06-03 β€” the same two-checkouts pattern chapter 06's own "Where we are" describes for its own opening, not something new to re-litigate here.

What does change, the moment the log resumes, is the goal. Chapter 06 was about the compiler resembling Haskell β€” modules, a Prelude, comprehensions. This chapter is about the compiler becoming a legal program written in the dialect it itself accepts: no type classes, no deriving, no fixity declarations, nothing GHC would compile without a comment doing the work instead. The chicken-and-egg mechanics of why that specific, narrower goal matters β€” and the concrete ladder of make targets MicroHs uses to prove it, once chapter 08 finishes wiring them up β€” are this book's new aux doc, bootstrapping-a-compiler.md; read it alongside this chapter's "What happened" below, which walks the same July grind chronologically.

What happened

The range 41d6a7e8..a824a175 is 90 commits (one of them, d128fb6f, a merge) landing on eleven calendar days spread across eighteen: 07-01 through 07-03 and 07-05 through 07-07 in one cluster, then a lone commit on 07-11, then 07-15 through 07-18 closing it out β€” a shape with its own internal gaps, not a steady eighteen-day burn.

The return: compiling with a local lib

The first evening back, 07-01, opens short and mechanical: 44004020 ("Small fixes.") adds -package mtl directly to the ghc ... -o $(BIN)/uhs build line and folds bin/* into clean; six minutes later, d128fb6f merges in the stale branch mentioned above.

The real re-launch follows fifty-two minutes after that, still the same evening. ff3837b9 ("Allow ghc compilation with local lib.") adds a brand-new top-level directory, ghc/, alongside a matching lib/Primitives.hs β€” two implementations of the same interface, one per audience. lib/Primitives.hs is the dialect uhs itself reads, built on the primitive "..." FFI-style calls the runtime already understands (ff3837b9:lib/Primitives.hs lines 3–4, quoted):

primIntAdd  = primitive "+"
primIntSub  = primitive "-"

ghc/Primitives.hs is the same names, this time real GHC functions over GHC's own Int and IO, for when GHC compiles the library directly (ff3837b9:ghc/Primitives.hs lines 10–11, quoted):

primIntAdd :: Int -> Int -> Int
primIntAdd = (+)

Every function lib/'s modules call through Primitives now has two bodies, chosen by which compiler's search path finds it first. Three minutes later, 2a95e71e ("Allow compilation with local lib.") wires a new Makefile target (a.out, later renamed) that compiles the whole lib/ tree plus Main.hs under GHC against the ghc/ shim instead of GHC's own base β€” this is the "local lib" build the aux doc's bootboottest traces back to. The same commit finally finishes what 2f89971e left mid-keystroke back in chapter 06: Translate.hs's comb "IO.h β€” an unterminated string literal that couldn't even lex β€” becomes a real, if still partial, primitive-ops table (2a95e71e:src/MicroHs/Translate.hs lines 60–67, quoted whole):

  , comb "IO.>>=" iobind
  , comb "IO.>>" iothen
  , comb "IO.return" ioret
--  , comb "IO.getChar" getc
  , comb "IO.putChar" putc
  , comb "IO.stdin" stdin
  , comb "IO.stdout" stdout
  , comb "IO.stderr" stderr

An hour and fourteen minutes later, fcde46f4 ("Make it compile.") is a four-line Makefile fix β€” a stray $(BOOTDIR) missing from GHCFLAGS's include path β€” restoring the a.out target's own buildability, nothing more than its title claims. The evening closes with two small, honestly-titled commits: 27df3d65 ("Skip {- -} comments. Fix bugs.") and, forty-one seconds later, fe2ea98a ("More ignore"), a nested-comment parsing fix and a .gitignore entry. 81683158 ("Better Makefile") follows the next day.

"Make it compile with uhs"

Two days after that first evening, on 07-03, a four-commit run of small refactors (688dfe52 "More functions.", 4e2bb772 "Refactor symbol table handling.", 50e30797 "Refactor symbol table creation.", a3302100 "Make module definitions shadow imports.") leads into the first commit actually titled for the new goal: d6c25f00 ("Make ParserComb compile with uhs."), 408 lines changed across ParserComb.hs and Parse.hs. The Functor/Applicative/Monad/Alternative instances chapter 02's own aux doc, parser-combinators.md, walks in detail are gone; in their place, plain top-level functions with the same names and the class methods' old bodies, newtype downgraded to data because uhs doesn't have newtype either, and the one place a real Show instance existed now sits as a dead comment (d6c25f00:src/Text/ParserComb.hs lines 65–70, quoted whole):

data Prsr s a = P ((String, s) -> Res s a)
runP :: Prsr s a -> ((String, s) -> Res s a)
runP pp =
  case pp of
    P p -> p
--instance Show (Prsr s a) where show _ = "<<Prsr>>"

The monadic bind itself moves from an instance Monad method to a free function of the same name, and its list-comprehension pattern binder ((a, u) <- aus) becomes an explicit let-destructuring inside a generator clause instead β€” uhs's comprehension desugaring at this point only ever binds one name at a time straight from a lambda argument, not a tuple pattern mid-comprehension (d6c25f00:src/Text/ParserComb.hs lines 75–82, quoted whole):

--Xinfixl 1 >>=
(>>=) :: Prsr s a -> (a -> Prsr s b) -> Prsr s b
(>>=) p k = P $ \ t ->
    case runP p t of
      Many aus plf ->
        let { xss = [ runP (k a) u | au <- aus, let { (a, u) = au } ] }
        in  case unzip [ (rs, lf) | xs <- xss, let { Many rs lf = xs } ] of
              (rss, lfs) -> Many (concat rss) (longests $ plf : lfs)

The --Xinfixl 1 >>= comment above it is the marker the aux doc explains in full: invisible to uhs, uncommented into a real infixl declaration for GHC by the convertX.sh preprocessor. Two days later, f37b389f ("Make it compile with uhs", 397 lines in Parse.hs) repeats the same treatment on the parser proper β€” every deriving (Show) becomes --Xderiving (Show), and elem/== calls that leaned on Eq get replaced by the explicit, monomorphic elemBy/eqString helpers src/Compat.hs supplies for exactly this purpose.

Later that same 07-05, bd8a47d7 ("Make the Exp module compile with uhs.") does the same to Exp.hs, and this time the commit says the quiet part out loud in its own body: "This makes it quite ugly. :(". The PatternSynonyms extension Exp.hs used since chapter 02 (pattern App2 x y z = App (App x y) z, and friends) is commented out wholesale, and every place that leaned on those patterns for readable multi-clause matches grows an explicit helper that returns an ordinary sum type and a case over it (bd8a47d7:src/MicroHs/Exp.hs lines 19–28, quoted whole):

data MaybeApp = NotApp | IsApp Exp Exp

getApp :: Exp -> MaybeApp
getApp ae =
  case ae of
    Var _   -> NotApp
    App f a -> IsApp f a
    Lam _ _ -> NotApp
    Int _   -> NotApp
    Prim _  -> NotApp

cS, the S-combinator constructor that chapter 02's optimizer built out of six terse pattern-matched equations (bd8a47d7:src/MicroHs/Exp.hs lines 189–194 show the old form, still present in the same diff as dead code below the new one), is rewritten around getApp and isPrim calls into three separate functions (cS/cS2/cS3, roughly 47 lines combined) chained by nested if/case β€” same optimization, far more code, because pattern-matching on PatternSynonyms constructors is gone and there's no other way left to ask "is this expression an application of K" without spelling out the whole case analysis by hand. Three commits, one throughline: nothing here changes what the compiler does β€” every rewrite is behavior- preserving β€” only what vocabulary it's allowed to use to say it.

515664d1: wrapping IO so unsafeCoerce behaves

Later the same day d6c25f00 lands β€” the very next commit, nine hours on, at 21:28 that evening β€” 515664d1 ("Make unsafeCoerce work by wrapping IO in a data constructor.") fixes a narrower problem: Translate.hs's trans function turns every compiled combinator term into a GHC value via unsafeCoerce :: Exp -> Any, and GHC's optimizer is free to treat a bare, coerced IO a action differently from an ordinary value β€” nothing forces it to actually stay an unevaluated IO action across the coercion boundary. The fix is a one-constructor wrapper that gives GHC something concrete to hold onto (515664d1:src/MicroHs/Translate.hs line 14, quoted, alongside its use seven lines later):

data DIO a = DIO { unDIO :: IO a }
  in  unDIO $ unsafeCoerce $ look mainName

"Running IO still doesn't work. :("

Fifteen minutes after 515664d1, 6f08f029 carries the frustration straight into its own commit title: "Running IO still doesn't work. :(". The diff is not, despite the title, a shrug β€” it's a real fix to the three functions 515664d1's new DIO wrapper had left half-done. Before this commit, iobind a k = unDIO a >>= k tries to bind a plain GHC IO action's result straight into a continuation typed a -> DIO b, which doesn't type-check as written; 6f08f029 corrects all three combinators to unwrap and rewrap DIO consistently (6f08f029:src/MicroHs/Translate.hs lines 79–84, quoted whole):

    iobind :: DIO a -> (a -> DIO b) -> DIO b
    iobind a k = DIO (unDIO a >>= \ x -> unDIO (k x))
    iothen :: DIO a -> DIO b -> DIO b
    iothen a b = DIO (unDIO a >> unDIO b)
    ioret :: a -> DIO a
    ioret a = DIO (return a)

A real correctness fix, landing under a title that reads like defeat β€” the sad face is honest about the overall state (something downstream still doesn't run), not about this particular diff's quality.

Whatever was still broken after 6f08f029 waits until the next session: nothing lands 07-04, and the next sixteen commits β€” everything from 67715b16 ("Add 'not'") through c275f468 ("More testing") β€” all carry timestamps between 12:22:03 and 12:58:43 on 07-05, a 36-minute window two days later. Inside that window sits b808bc73 ("Fix IO primitives."), which is where the actual IO fix lives. The bug is a convention mismatch across the primitives boundary. lib/System/IO.hs's hGetChar wrapper reads a character through P.primHGetChar and checks for an end-of-file sentinel (1abb181e:lib/System/IO.hs lines 25–30, quoted whole β€” 1abb181e is b808bc73's parent):

hGetChar :: Handle -> IO Char
hGetChar h = do
  c <- P.primHGetChar h
  case ord c == negate 1 of
    False -> return c
    True  -> error "hGetChar: EOF"

Under uhs's own runtime that convention works: ord is the identity function (chapter 06's CHAR removal made it so), and eval.c's IO.getChar really does hand back -1 at end of file. But ghc/Primitives.hs's shim had simply aliased GHC's own functions β€” and left two others unwritten (1abb181e:ghc/Primitives.hs lines 66–69, quoted whole):

primHPutChar      = hPutChar
primHGetChar      = hGetChar
primOpenFile      = undefined
primIsNullHandle  = undefined

GHC's hGetChar never returns a sentinel β€” it throws an exception at end of file β€” so the wrapper's ord c == negate 1 branch could never fire under the GHC build (no Char's ord is negative there), and primOpenFile had been undefined since ff3837b9 first wrote it on 07-01. The code type-checks fine on both paths; it's the semantics of the shim that don't match the runtime's contract. b808bc73 makes the shim honor the same convention the uhs runtime speaks β€” Int character codes with -1 at EOF, checked explicitly via hIsEOF (b808bc73:ghc/Primitives.hs lines 66–71, quoted whole):

primHPutChar     :: Handle -> Int -> IO ()
primHPutChar h c  = hPutChar h (toEnum c)
primHGetChar     :: Handle -> IO Int
primHGetChar h    = do eof <- hIsEOF h; if eof then pure (-1) else fromEnum <$> hGetChar h
primOpenFile     :: String -> Int -> IO Handle
primOpenFile s m  = openFile s (case m of 0->ReadMode; 1->WriteMode; 2->AppendMode; 3->ReadWriteMode)

and, in the same commit, adjusts lib/System/IO.hs's wrappers to match: the sentinel test becomes c == negate 1 on the raw Int, with an explicit chr/ord conversion at the boundary β€” b808bc73:lib/System/IO.hs line 31, quoted whole: hPutChar h c = P.primHPutChar h (ord c). Unlike 6f08f029, this title matches its diff exactly β€” it fixes IO primitives, on both sides of the boundary, and nothing else is smuggled in.

The diet

Inside that same 07-05 burst, three more threads close out the "diet" the aux doc names directly. a721e046 ("Use Int instead of Integer") narrows Expr's EInt field from Integer to Int (a721e046:src/MicroHs/Parse.hs line 49: | EInt Int) and drops the now-unneeded fromInteger call in the desugarer (a721e046:src/MicroHs/Desugar.hs line 104: dsExpr _ (EInt i) = Int i); 2ff2a2e9 ("More s/Integer/Int/") mops up one remaining Parse.hs site the same day. f113210f ("Use our own Map.") replaces qualified Data.Map with a new src/MicroHs/Map.hs β€” a plain association-list wrapper that takes its equality test as an ordinary function argument rather than an Ord constraint, candid about its own limits in a comment beside the type (f113210f:src/MicroHs/Map.hs lines 1–33, quoted whole):

module MicroHs.Map(module MicroHs.Map) where
import Prelude
--Ximport Compat

-- This is a pretty bad implementation.
data Map k v = Map [(k, v)]
  --Xderiving(Show)

insert :: k -> v -> Map k v -> Map k v
insert k v (Map kvs) = Map ((k, v):kvs)

fromListWith :: (k -> k -> Bool) -> (v -> v -> v) -> [(k, v)] -> Map k v
fromListWith eq un =
  let
    ins ikv akvs =
      case akvs of
        [] -> [ikv]
        kv : kvs ->
          if eq (fst ikv) (fst kv) then
            (fst ikv, un (snd ikv) (snd kv)) : kvs
          else
            kv : ins ikv kvs
  in
     Map . foldr ins []

fromList :: [(k, v)] -> Map k v
fromList = Map

union :: Map k v -> Map k v -> Map k v
union (Map kvs1) (Map kvs2) = Map (kvs1 ++ kvs2)

lookup :: (k -> k -> Bool) -> k -> Map k v -> Maybe v
lookup eq k (Map kvs) = lookupBy eq k kvs

And fixity: three commits inside the same burst β€” edfb9bde ("Add fixties"), 1200d1a3/56ad54ae (both "--Yinfix") β€” sprinkle --Yinfixr/--Yinfixl/--Yinfix comments through lib/Data/Bool.hs, lib/Data/Int.hs, and lib/Data/Function.hs (edfb9bde:lib/Data/Bool.hs lines 7–8, quoted):

--Yinfixr 2 ||
(||) :: Bool -> Bool -> Bool

These are not read by uhs at all β€” its own parser resolves every operator's precedence from a fixed, hand-written ladder in pExpr (unchanged by any commit in this whole range), not from declarations in the library source. The --Y marker exists purely so the other GHC build β€” the "local lib" path 2a95e71e set up, which compiles lib/Data/Bool.hs directly under GHC via the convertY.sh preprocessor β€” sees a real infixr 2 || and parses (||)'s own definition and every call site the way the library author intended. The mechanism in full, including why two separate markers (--X/--Y) exist for two separate GHC builds, is the aux doc's subject, not repeated here.

59b8b1ab: a rename that isn't just a rename

Still inside the same 16-commit, 36-minute burst (67715b16 through c275f468, above), 59b8b1ab lands at 12:57:39 under the title "Fix name" β€” no trailing period. It does contain a rename β€” lib/PreludeNOIO.hs becomes lib/PreludeNoIO.hs, fixing an inconsistent capitalization. But the same commit also adds three files the title never mentions: A.hs (9 lines, testing operator sections against +/-), Test.hs (39 lines, an early ancestor of what later becomes the project's throwaway Main.hs scratch harness), and t.hs (2 lines, a fixture Test.hs reads). Fifty lines of new Haskell, three new files, under a two-word title about one file's casing.

8ab69cb0: a squash! that never got squashed

An hour and nine minutes later, in the next cluster that same afternoon (13:22:18–14:07:11), 46c4309f ("infixr 9 .") adds one more --Yinfixr 9 . comment to Data.Function, 14:07:00. Eleven seconds after it, 8ab69cb0's commit title is squash! infixr 9 . β€” Git's own convention for a commit meant to be folded into an earlier one (here, 46c4309f) by git rebase --autosquash. That rebase never ran: 8ab69cb0 sits in this repository's history as its own commit, forever, eleven seconds after the commit it was named to merge into.

Its diff has nothing to do with infixr 9 . at all. Six files change: a new alltest Makefile target; a new enumFrom in lib/Data/List.hs; two new helpers in src/Compat.hs; four dead --X comment lines removed from src/Text/ParserComb.hs; a deriving-stripped, case- rewritten Exp.hs (the same "make it compile with uhs" treatment bd8a47d7 gives the rest of the file two commits later); and, in src/MicroHs/Main.hs, the Lbl Int Exp constructor's last use quietly disappears β€” the labeled-definition wrapper App2 CT (Lbl i (subst e)) r becomes a hand-built string, with the old call left behind as a dead comment right next to its replacement (8ab69cb0:src/MicroHs/Main.hs lines 31–33, quoted whole):

      def :: ((Ident, Exp), Int) -> String -> String
      def ((_, e), i) r = "(($T :" ++ showInt i ++ " " ++ toStringP (subst e) ++ ") " ++ r ++ ")"
        -- App2 CT (Lbl i (subst e)) r

A whole data constructor retired, in a commit whose title names an unrelated one-line fixity comment it was never even merged into β€” the same lesson chapter 06 drew from 2f89971e's stowaway Translate.hs: read the diff, not just the subject line.

The last two weeks

The range closes with a longer, more scattered stretch: 07-06 and 07-07 bring a getArgs preparation and a pattern-matching cleanup, 6b5741e1 ("Get rid of PTuple constructor."); then four quiet days, one lone commit breaking them, 4ae4fd20 ("Add copyright") on 07-11, adding a BSD-style LICENSE file, and four quiet days more before 07-15 resumes with a358eaa6 ("Add copyright.") stamping a two-line header onto 27 source files, followed 35 minutes later by 52ee83e2 ("Start of complex patterns.") β€” pattern matching resuming after the 07-07 cleanup β€” and 28 minutes after that by d514d2d9 swapping the whole project to the Apache license instead β€” copyright housekeeping and pattern-matching work interleaved the same afternoon. 07-16 and 07-17 grind through more of the "make it compile with uhs" series by name (a36ca755 "Make Desugar compile with uhs", 7e7c18d1 "Make Compile module work with uhs", 5e462798 "Make Main work with uhs."), getArgs support, and β€” honestly titled, no surprises in the diff β€” e61ea002 ("A hack to make openFile compatible with uhs."). The range's last commit, a824a175 ("Check for stack overflow."), 07-18 15:04:09, replaces the C runtime's fixed-size stack array with a malloc'd one sized by a new -K flag β€” the same flag the aux doc's bootcombtest target invokes (-K1000000) to survive interpreting the compiler's own bulky combinator file. What that target actually running looks like is chapter 08's story.

Why it matters

Self-hosting is a forcing function with an unusually sharp feedback signal: a gap in the compiler's own feature set doesn't show up as a missing test case someone has to think to write, it shows up as "my own compiler's source won't compile" the moment that gap sits between the compiler and itself. Every thread this chapter walks β€” no type classes, no deriving, a hand-rolled Map, Int instead of Integer, fixity resolved by a fixed parser ladder instead of declarations β€” is something the compiler's own source needed and didn't have, discovered one "make it compile with uhs" commit at a time rather than planned in advance. The TODO list writes itself: point the compiler at its own three-thousand-odd lines (src//lib/ combined, .hs files only, at this range's close) and see what breaks.

Notebook margin

  • "More functions." (and its unpunctuated twin, "More functions") opens 13 separate commits across this range's 90 β€” the grind's own rhythm, the same small, load-bearing filler chapter 06 first named.
  • 8ab69cb0's title, squash! infixr 9 ., is Git's own convention for "please fold me into the commit with this subject via git rebase --autosquash" β€” a request nobody ever ran. It's still sitting there, its own permanent commit, eleven seconds after the commit it asked to disappear into.
  • The 16 commits from 67715b16 through c275f468 all carry timestamps inside one 36-minute window on 07-05 (12:22:03–12:58:43) β€” the tightest cluster in this chapter's range, and the one that happens to contain both the real IO fix (b808bc73) and one of the two undisclosed-extras commits above (59b8b1ab, at 12:57:39). The other, 8ab69cb0, sits in the very next cluster that same afternoon (13:22:18–14:07:11).
  • The copyright thread took two license files four days apart: a BSD-style LICENSE on 07-11 (4ae4fd20), then an Apache-license swap on 07-15 (d514d2d9), 64 minutes after 27 files got copyright headers stamped on them by a358eaa6.

Try it

Anchor: f113210f ("Use our own Map."), 2023-07-05 20:55:37 β€” chosen because it's the last commit of that day's third, late-evening session (6c9d021f through f113210f, 19:34–20:55), sitting just after bd8a47d7's Exp-module rewrite and before the next day's getArgs work, and because β€” checked directly, not assumed β€” it builds cleanly as written. Convention (see 00-overview.md):

git worktree add /tmp/mhs-f113210f f113210f
cd /tmp/mhs-f113210f

This commit's Makefile builds $(BIN)/uhs from src/*/*.hs through the convertX.sh preprocessor described in the aux doc, and $(BIN)/eval from src/runtime/eval.c β€” both built and run below exactly as written, no patch needed:

mkdir -p bin
nix develop $MICROHS -c make bin/uhs
[...]
[10 of 11] Compiling Main             ( src/MicroHs/Main.hs, ghc-out/Main.o )
[11 of 11] Linking bin/uhs

(a run of -Wx-partial warnings about head/tail, one per call site, omitted β€” the same harmless category earlier chapters' Build notes already flag).

nix develop $MICROHS -c make bin/eval
gcc -Wall -O3 src/runtime/eval.c -o bin/eval

Exercise 1: run the ghc-built uhs on one of its own source modules

This commit's top-level Main.hs is the project's own scratch harness β€” it imports MicroHs.Parse, one of the compiler's real source modules under src/MicroHs/, and re-parses a tiny fixture file, t.hs, through it. Compiling it needs both -ilib (for Prelude) and -isrc (for MicroHs.Parse itself) β€” exactly the Makefile's own uhstest target:

nix develop $MICROHS -c ./bin/uhs -ilib -isrc Main

This exits silently and writes out.comb. A trimmed look at the file (it's one line, 32590 bytes on this build; head and tail shown, ... marking everything omitted between them):

(($T :0 _198) (($T :1 (_0 (($O 117) (($O 110) (($O 100) (($O 101) (($O 102) ...
...
))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

Running it through bin/eval confirms the whole pipeline β€” GHC-built uhs, compiling a module that itself imports a real compiler source file, producing a .comb file eval can run β€” actually works at this commit:

nix develop $MICROHS -c ./bin/eval
Start
File read: t.hs

module T(
module T

) where

That's Main.hs's own putStrLns ("Start", then the file-read message) followed by showEModule's rendering of t.hs's one-line module, module T(module T) where β€” parsed by MicroHs.Parse, compiled by a GHC-built uhs, run by eval.

Exercise 2: what typeclass usage survives in src/

nix develop $MICROHS -c grep -rn "class \|instance " src/
src/Text/ParserComb.hs:67:--instance Show (Prsr s a) where show _ = "<<Prsr>>"

One hit, across every .hs file under src/MicroHs/ and src/Text/ParserComb.hs combined β€” and it's commented out, the fossil d6c25f00 left behind when it rewrote ParserComb.hs's instances into plain functions (quoted in "What happened," above). Zero live class declarations, zero live instance declarations anywhere in the compiler's own source at this commit. The same search over lib/ (the library uhs itself compiles programs against) comes back empty too β€” not even a dead comment there.

Exercise 3: what convertX.sh/convertY.sh paper over

cat convertX.sh
cat convertY.sh
#!/bin/sh
( echo "{-# LINE 1 \"$1\" #-}" ; sed -e 's/--X//' $2 ) > $3
#!/bin/sh
( echo "{-# LINE 1 \"$1\" #-}" ; sed -e 's/--Y//' $2 ) > $3

Two lines each, and identical but for one character: strip a marker (--X or --Y) out of every line of the file, after stamping a {-# LINE #-} pragma so GHC's own error messages still point at the original file and line number. The gap they paper over is everything GHC's Haskell needs that uhs's Haskell doesn't have a way to say: real hiding clauses on import Prelude (so GHC's own Monad/Applicative methods don't collide with this project's hand-rolled >>=/pure), real infixl/infixr/infix declarations (so GHC's parser gives operators like (||) the precedence their definition assumes), real deriving (Show) clauses. Every one of those, in this chapter's source, is written as an ordinary two-character-prefixed comment β€” invisible to uhs's own parser, which just sees -- and stops reading β€” and turned back into live code only when one of these two scripts runs as GHC's own preprocessor first.

Clean up:

git worktree remove --force /tmp/mhs-f113210f