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 viagit 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
67715b16throughc275f468all 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
LICENSEon 07-11 (4ae4fd20), then an Apache-license swap on 07-15 (d514d2d9), 64 minutes after 27 files got copyright headers stamped on them bya358eaa6.
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