This checklist is ordered by when to do things:
- Pre-Rewrite Prep — easy wins that make writing the compiler nicer
- Stdlib Blockers — required data structures and functions
- Bootstrap Phases — the actual compiler rewrite
- Post-Bootstrap — optimizations, ergonomics, nice-to-haves
These are relatively easy to implement and will significantly improve the experience of writing the self-hosted compiler. Do these first.
Without guards, AST processing requires nested matches everywhere:
-- Without guards (painful)
match (expr) {
BinaryExpr(op, l, r) => {
match (op == "+") {
true => ...,
false => match (op == "-") { ... }
}
}
}
-- With guards (clean)
match (expr) {
BinaryExpr(op, l, r) when op == "+" => ...,
BinaryExpr(op, l, r) when op == "-" => ...,
}
- Parser:
Pattern when condition => body - AST: add
guard?: Exprto MatchPatternArm - Type checker: check guard is Bool
- Exhaustiveness: guards make patterns potentially non-exhaustive
- Codegen: emit conditional check after pattern match
You'll write hundreds of list constructions/patterns in the compiler:
-- Without syntax (tedious)
Link(1, Link(2, Link(3, Empty)))
match (tokens) { Link(a, Link(b, rest)) => ... }
-- With syntax (readable)
[1, 2, 3]
match (tokens) { [a, b, ...rest] => ... }
- Parser:
[1, 2, 3]as list literal - Parser:
[head, ...tail]spread syntax - Desugar to
Link(1, Link(2, Link(3, Empty)))
- Parser:
[a, b, c]in patterns - Parser:
[first, ...rest]spread in patterns - Parser:
[a, b, ..._]ignore rest - Desugar to constructor patterns
AST transformations constantly create modified nodes:
-- Without spread (error-prone, verbose)
.{ kind = oldNode.kind, value = oldNode.value, id = oldNode.id, span = newSpan }
-- With spread (clean)
.{ ..oldNode, span = newSpan }
- Parser:
.{ ..source, field = value } - Type checker: source must be same record type
- Codegen: copy all fields, override specified ones
-- Without punning
.{ name = name, age = age, span = span }
-- With punning
.{ name, age, span }
- Parser:
.{ name }expands to.{ name = name } - Works in record construction
For internal compiler errors (ICE):
match (impossible) {
_ => Panic("unreachable: this should never happen")
}
- Parser:
fail "message" - Type:
fail : forall a. String -> a(bottom type) - Runtime: halt with error message
-- Without operator
stringConcat(stringConcat(a, " "), b)
-- With operator
a ++ " " ++ b
- Define
++infix operator for strings -
infixl 5 ++ = stringConcat
These help but aren't as impactful for compiler code:
-
A | B => ...— share handler for multiple patterns
-
pattern as name— bind whole match to name
-
value |> fn1 |> fn2— nice for transforms but not essential
These must exist before you can write the compiler.
A compiler uses maps/sets everywhere: environments, scopes, operator tables, visited sets, free vars.
-
type StringMap<V>— association list keyed by String -
stringMapEmpty : StringMap<V> -
stringMapInsert : (String, V, StringMap<V>) -> StringMap<V> -
stringMapLookup : (String, StringMap<V>) -> Option<V> -
stringMapContains : (String, StringMap<V>) -> Bool -
stringMapRemove : (String, StringMap<V>) -> StringMap<V> -
stringMapToList : StringMap<V> -> List<(String, V)> -
stringMapFromList : List<(String, V)> -> StringMap<V> -
stringMapKeys : StringMap<V> -> List<String> -
stringMapValues : StringMap<V> -> List<V> -
stringMapMap : (V -> W, StringMap<V>) -> StringMap<W> -
stringMapFold : ((Acc, String, V) -> Acc, Acc, StringMap<V>) -> Acc -
stringMapUnion : (StringMap<V>, StringMap<V>) -> StringMap<V>
-
type StringSet— association list -
stringSetEmpty : StringSet -
stringSetInsert : (String, StringSet) -> StringSet -
stringSetContains : (String, StringSet) -> Bool -
stringSetRemove : (String, StringSet) -> StringSet -
stringSetToList : StringSet -> List<String> -
stringSetFromList : List<String> -> StringSet -
stringSetUnion : (StringSet, StringSet) -> StringSet -
stringSetDifference : (StringSet, StringSet) -> StringSet
-
type IntMap<V> -
intMapEmpty,intMapInsert,intMapLookup,intMapContains
-
type IntSet -
intSetEmpty,intSetInsert,intSetContains,intSetUnion
Note: Association-list is O(n) but fine for bootstrap. Upgrade to balanced tree later.
-
stringLength : String -> Int -
stringCharAt : (String, Int) -> Option<Char> -
stringSubstring : (String, Int, Int) -> String -
stringIndexOf : (Char, String) -> Option<Int> -
stringStartsWith : (String, String) -> Bool -
stringEndsWith : (String, String) -> Bool -
stringContains : (String, String) -> Bool -
stringToList : String -> List<Char> -
stringFromList : List<Char> -> String -
stringIsEmpty : String -> Bool
Use the "accumulate list, join once" pattern:
-
stringJoin : (String, List<String>) -> String -
stringConcat : (String, String) -> String
-
spanSlice : (String, Span) -> String -
spanLineCol : (String, Int) -> (Int, Int)— for diagnostics
-
type Span = { start: Int, end: Int } -
type Diagnostic = { span: Span, message: String, severity: Severity, notes: List<DiagnosticNote> } -
type DiagnosticNote = { span: Option<Span>, message: String } -
type Severity = Error | Warning | Info | Hint -
diagnosticPrettyPrint : (String, Diagnostic) -> String
Verify/add these:
-
listHead : List<A> -> Option<A> -
listTail : List<A> -> Option<List<A>> -
listNth : (Int, List<A>) -> Option<A> -
listZip : (List<A>, List<B>) -> List<(A, B)> -
listZipWith : ((A, B) -> C, List<A>, List<B>) -> List<C> -
listFlatten : List<List<A>> -> List<A> -
listFilterMap : (A -> Option<B>, List<A>) -> List<B> -
listPartition : (A -> Bool, List<A>) -> (List<A>, List<A>) -
listFindIndex : (A -> Bool, List<A>) -> Option<Int> -
listIsEmpty : List<A> -> Bool -
listMapi : ((Int, A) -> B, List<A>) -> List<B>
-
resultIsOk : IResult<A, E> -> Bool -
resultIsErr : IResult<A, E> -> Bool -
resultToOption : IResult<A, E> -> Option<A> -
resultSequence : List<IResult<A, E>> -> IResult<List<A>, E> -
resultTraverse : (A -> IResult<B, E>, List<A>) -> IResult<List<B>, E> -
optionToResult : (E, Option<A>) -> IResult<A, E> -
optionSequence : List<Option<A>> -> Option<List<A>> -
optionTraverse : (A -> Option<B>, List<A>) -> Option<List<B>>
Thread state for pure FP:
-
type IdGen = { next: Int } -
freshId : IdGen -> (Int, IdGen)
-
assert : Bool -> () -
assertEqual : (A, A) -> () -
test : (String, () -> ()) -> () -
runTests : List<Test> -> ()
- File IO:
readFile,writeFile - CLI args:
argv - Module loading basics
-
print/ stderr output
Write the actual compiler in Workman.
- Token type definition
- Character stream (index into string)
- Keyword recognition (use StringSet)
- Operator scanning
- String/char/number literal parsing
- Comment handling
- Span tracking
- Error recovery with diagnostics
- AST type definitions (big sum type)
- Recursive descent parser
- Operator precedence (Pratt or shunting-yard)
- Pattern parsing
- Type expression parsing
- Error recovery
- Source span on every node
- Type representation
- Type environment (StringMap)
- Unification algorithm
- Constraint generation
- Constraint solving
- Generalization / instantiation
- Pattern exhaustiveness
- Infection/effect tracking
- Target: JS (easiest to start)
- IR or direct emit
- String building for output
Do these after the compiler works.
- Replace association-list with AVL or Red-Black tree
- O(log n) instead of O(n) lookups
- Same API, swap implementation
- Requires hashing strategy
- Only if tree perf is insufficient
-
let mutmutable bindings - Reassignment:
x = newValue - Compound assignment:
x += 1 - Mutable record fields
- Early
return -
while/forloops -
break/continue
- Mutable fixed-size arrays
- O(1) index access
- Default arguments
- Named arguments at call site
- Range syntax:
0..10 - If-let:
if let Some(x) = option { ... }
- Equality/ordering strategy
-
type Map<K, V>with generic keys -
type Set<A>with generic elements
-
listReject,listTakeWhile,listDropWhile -
listSpan,listIntersperse,listUnzip -
listReplicate,listSingleton -
listFilteri,listInit
-
charIsDigit,charIsAlpha,charIsAlphaNum -
charIsWhitespace,charIsUpper,charIsLower -
charToUpper,charToLower -
charToInt,charFromInt
-
intAbs,intMin,intMax,intClamp -
intSign,intPow -
intToString,intFromString -
intRange : (Int, Int) -> List<Int>
-
fst,snd,swap,curry,uncurry -
identity,const,flip,compose,pipe
-
Float,BigInt,Rational
- "Did you mean X?" suggestions
- Multi-span errors
- Contextual hints
- Pretty-print AST
- Pretty-print types
- Type inference trace mode
- Doc comments (
--| ...) - Generate docs from source
- Proper module resolution
- Dependency declaration
- M0: Quick language wins (guards, list literals, record spread, fail)
- M1: StringMap + StringSet + IntMap + IntSet
- M2: String ops + Diagnostics + List ops complete
- M3: Lexer written in Workman
- M4: Parser written in Workman
- M5: Type checker written in Workman
- M6: Full self-hosting achieved 🎉
- M7: Balanced-tree Map/Set (performance)
- M8: Extended ergonomics (mutability, arrays, etc.)
| Category | Done | Total | Progress |
|---|---|---|---|
| Phase 0: Language Wins | |||
| Match guards | 5 | 5 | 100% |
| List literals (expr) | 3 | 3 | 100% |
| List literals (pattern) | 4 | 4 | 100% |
| Record spread | 3 | 3 | 100% |
| Record punning | 2 | 2 | 100% |
fail expression |
0 | 3 | 0% |
++ operator |
0 | 1 | 0% |
| Phase 1: Stdlib | |||
| StringMap | 0 | 13 | 0% |
| StringSet | 0 | 9 | 0% |
| IntMap/IntSet | 0 | 8 | 0% |
| String ops | ? | 12 | ?% |
| Diagnostics | 0 | 5 | 0% |
| List ops | ~15 | 20 | ~75% |
| Result/Option | ~8 | 15 | ~53% |
| Testing harness | 0 | 4 | 0% |
| Decision | Rationale |
|---|---|
| Guards before bootstrap | Makes AST code dramatically cleaner |
| List literals before bootstrap | Constant use in compiler code |
| Record spread before bootstrap | AST transformations are much nicer |
| String-keyed maps first | Compilers mostly use string keys |
| Association-list Map/Set | Simple, correct, upgrade later |
stringJoin pattern |
Pure FP, simple, fast enough |
No let mut for bootstrap |
Compiler can be written functionally |
| Balanced tree after bootstrap | Don't block on optimization |
- Phase 0 is worth the investment — these features pay off immediately in cleaner compiler code
- Association-list Map is O(n) but fine for <10k entries
- Thread state explicitly:
(result, newState) = transform(input, state) - Accumulate strings as
List<String>, join once at end - Test early and often
Last updated: 2026-01-07