32 KiB
Haskell-on-SX: mini-Haskell with real laziness
Mini-Haskell is the research-paper-worthy demo. Laziness is native to the SX runtime (thunks are already a first-class type); algebraic data types map onto tagged lists; typeclasses map onto dictionary passing; IO maps onto perform/resume. Hindley-Milner inference is the one real piece of new work.
End-state goal: a Haskell 98 subset that runs the small classic programs (sieve of Eratosthenes lazy stream, fibonacci as infinite list, naive quicksort, n-queens, expression evaluator) plus a ~150-test corpus.
Scope decisions (defaults — override)
- Standard: Haskell 98 subset. No GHC extensions (no
DataKinds, noGADTs, noTypeFamilies, noTemplateHaskell). - Phase 1-3 are untyped — we get the evaluator right first with laziness + ADTs, then add HM inference in phase 4. This is deliberate: typing is the hard bit and will take a full phase on its own.
- Typeclasses: dictionary passing, no overlap, no orphan instances. Added in phase 5.
- Layout rule: yes — phase 1 implements Haskell's indentation-sensitive parsing (painful but required).
- Test corpus: custom. No GHC test suite. Bundle classic programs + ~100 hand-written expression-level tests + mini Prelude tests.
Ground rules
- Scope: only
lib/haskell/**andplans/haskell-on-sx.md. No edits tospec/,hosts/,shared/, or other language dirs. - SX files:
sx-treeMCP tools only. - Architecture: Haskell source → AST → desugared-core → SX AST → CEK. Thunks on the SX side provide laziness natively.
- Commits: one feature per commit. Keep
## Progress logupdated.
Architecture sketch
Haskell source
│
▼
lib/haskell/tokenizer.sx — idents, operators, layout-sensitive indentation
│
▼
lib/haskell/parser.sx — AST: modules, data decls, type sigs, fn clauses, expressions
│
▼
lib/haskell/desugar.sx — surface → core: case-of-case, do-notation, list comp, guards
│
▼
lib/haskell/transpile.sx — core → SX AST, wrapping everything in thunks for laziness
│
▼
lib/haskell/runtime.sx — force, ADT constructors, Prelude, typeclass dicts (phase 5+)
│
▼
existing CEK / VM
Key mappings:
- Laziness = every function argument is an SX thunk;
forceis WHNF reduction. SX already hasmake-thunkfrom the trampolining evaluator — we reuse it. - Pattern match = forces the scrutinee to WHNF, then structural match on the tag
- ADT =
data Maybe a = Nothing | Just acompiles to tagged lists:(:Nothing)and(:Just <thunk>) - Typeclass = each class becomes a record type; each instance becomes a record value; each method becomes a projection; the elaborator inserts the dict at each call site (phase 5)
- IO =
IO ais a functionWorld -> (a, World)internally; in practice usesperform/resumefor actual side effects - Layout = offside rule; inserted virtual braces + semis during a lexer-parser feedback pass
Roadmap
Phase 1 — tokenizer + parser + layout rule
- Tokenizer: reserved words, qualified names, operators, numbers (int, float, Rational later), chars/strings, comments (
--and{-nested) - Layout algorithm: turn indentation into virtual
{,;,}tokens per Haskell 98 §10.3 - Parser (split into sub-items — implement one per iteration):
- Expressions: atoms, parens, tuples, lists, ranges, application, infix with full Haskell-98 precedence table, unary
-, backtick operators, lambdas,if,let case … ofanddo-notation expressions (plus minimal patterns needed for arms/binds: var, wildcard, literal, 0-arity and applied constructor, tuple, list)- Patterns — full:
aspatterns, nested, negative literal,~lazy, infix constructor (:/ consym), extend lambdas/let with non-var patterns - Top-level decls: function clauses (simple — no guards/where yet), pattern bindings, multi-name type signatures,
datawith type vars and recursive constructors,typesynonyms,newtype, fixity (infix/infixl/infixrwith optional precedence, comma-separated ops, backtick names). Types: vars / constructors / application /->(right-assoc) / tuples / lists.hk-parse-topentry. whereclauses + guards (on fun-clauses, case alts, and let/do-let bindings — with the let funclause shorthandlet f x = …now supported)- Module header + imports —
module NAME [exports] where …, qualified/as/hiding/explicit imports, operator exports,module Fooexports, dotted names, headerless-with-imports - List comprehensions + operator sections —
(op)/(op e)/(e op)(excluding-from right sections),[e | q1, q2, …]withq-gen/q-guard/q-letqualifiers
- Expressions: atoms, parens, tuples, lists, ranges, application, infix with full Haskell-98 precedence table, unary
- AST design modelled on GHC's HsSyn at a surface level — keyword-tagged lists cover modules/imports/decls/types/patterns/expressions; see parser.sx docstrings for the full node catalogue
- Unit tests in
lib/haskell/tests/parse.sx(43 tokenizer tests, all green)
Phase 2 — desugar + eager-ish eval + ADTs (untyped)
- Desugar: guards → nested
ifs;where→let; list comp →concatMap-based; do-notation stays for now (desugared in phase 3) datadeclarations register constructors in runtime- Pattern match (tag-based, value-level): atoms, vars, wildcards, constructor patterns,
aspatterns, nested - Evaluator (still strict internally — laziness in phase 3):
let,lambda, application,case, literals, constructors - 30+ eval tests in
lib/haskell/tests/eval.sx
Phase 3 — laziness + classic programs
- Transpile to thunk-wrapped SX: every application arg becomes
(make-thunk (lambda () <arg>)) force= SX eval-thunk-to-WHNF primitive- Pattern match forces scrutinee before matching
- Infinite structures:
repeat x,iterate f x,[1..], Fibonacci stream (sieve deferred — needs lazy++and is exercised underClassic programs) seq,deepseqfrom Prelude- Do-notation for a stub
IOmonad (just threading, no real side effects yet) - Classic programs in
lib/haskell/tests/programs/:fib.hs— infinite Fibonacci streamsieve.hs— lazy sieve of Eratosthenesquicksort.hs— naive QSnqueens.hscalculator.hs— parser combinator style expression evaluator
lib/haskell/conformance.sh+ runner;scoreboard.json+scoreboard.md- Target: 5/5 classic programs passing
Phase 4 — Hindley-Milner inference
- Algorithm W: unification + type schemes + generalisation + instantiation
- Report type errors with meaningful positions
- Reject untypeable programs that phase 3 was accepting
- Type-sig checking: user writes
f :: Int -> Int; verify - Let-polymorphism
- Unit tests: inference for 50+ expressions
Phase 5 — typeclasses (dictionary passing)
class/instancedeclarations- Dictionary-passing elaborator: inserts dict args at call sites
- Standard classes:
Eq,Ord,Show,Num,Functor,Monad,Applicative deriving (Eq, Show)for ADTs
Phase 6 — real IO + Prelude completion
- Real
IOmonad backed byperform/resume putStrLn,getLine,readFile,writeFile,print- Full-ish Prelude:
Maybe,Either,Listfunctions,Map-lite - Drive scoreboard toward 150+ passing
Progress log
Newest first.
-
2026-04-25 — Phase 3 do-notation + stub IO monad. Added a
hk-desugar-dopass that follows Haskell 98 §3.14 verbatim:do { e } = e,do { e ; ss } = e >> do { ss },do { p <- e ; ss } = e >>= \p -> do { ss }, anddo { let ds ; ss } = let ds in do { ss }. The desugarer's:dobranch now invokes this pass directly so the surface AST forms (:do-expr,:do-bind,:do-let) never reach the evaluator. IO is represented as a tagged value("IO" payload)—return(lazy builtin) wraps;>>=(lazy builtin) forces the action, unwraps, and calls the bound function on the payload;>>(lazy builtin) forces the action and returns the second one. All three are non-strict in their action arguments so deeply nested do-blocks don't walk the whole chain at construction time. 14 new tests inlib/haskell/tests/do-io.sxcover single-stmt do, single and multi-bind,>>sequencing (last action wins), do-let (single, multi, interleaved with bind), bind-to-Just, bind-to-tuple, do inside a top-level fun, nested do, and using(>>=)/(>>)directly as functions. 382/382 green. -
2026-04-25 — Phase 3
seq+deepseq. Built-ins were strict in all args by default (every collected thunk forced before invoking the underlying SX fn) — that defeatsseq's purpose, which is strict in its first argument and lazy in its second. Added a tinylazyflag on the builtin record (set by a newhk-mk-lazy-builtinconstructor) and routedhk-apply-builtinto skip the auto-force when the flag is true.seq a bcallshk-force athen returnsbunchanged so its laziness is preserved;deepseqdoes the same withhk-deep-force. 9 new tests inlib/haskell/tests/seq.sxcover primitive, computed, and let-bound first args, deepseq on a list /Just/ tuple, seq inside arithmetic, seq via a fun-clause, and[seq 1 10, seq 2 20]to confirm seq composes inside list literals. The lazy-when-unused negative case is also tested:let x = error "never" in 42 == 42. 368/368 green. -
2026-04-24 — Phase 3 infinite structures + Prelude. Two evaluator changes turn the lazy primitives into a working language:
- Op-form
:is now non-strict in both args —hk-eval-opspecial-cases it before the eager force-and-binop path, so a cons-cell holds two thunks. This is what makesrepeat x = x : repeat x,iterate f x = x : iterate f (f x), and the classicfibs = 0 : 1 : zipWith plus fibs (tail fibs)terminate when only a finite prefix is consumed. - Operators are now first-class values via a small
hk-make-binop-builtinhelper, so(+),(*),(==)etc. can be passed tozipWithandmap. Added range support across parser + evaluator:[from..to]and[from,next..to]evaluate eagerly viahk-build-range(handles step direction);[from..]parses to a new:range-fromnode that the evaluator desugars toiterate (+ 1) from. Newhk-load-into!runs the regular pipeline (parse → desugar → register data → bind decls) on a source string, andhk-init-envpreloadshk-prelude-srcwith the Phase-3 Prelude:head,tail,fst,snd,take,drop,repeat,iterate,length,map,filter,zipWith, plusfibsandplus. 25 new tests inlib/haskell/tests/infinite.sx, includingtake 10 fibs == [0,1,1,2,3,5,8,13,21,34],head (drop 99 [1..]),iterate (\x -> x * 2) 1powers of two, user-definedones = 1 : ones,naturalsFrom, range edge cases, composedmap/filter, and a custommySum. 359/359 green. Sieve of Eratosthenes is deferred — it needs lazy++plus amodprimitive — and lives underClassic programsanyway.
- Op-form
-
2026-04-24 — Phase 3 laziness foundation. Added a thunk type to
lib/haskell/eval.sx(hk-mk-thunk/hk-is-thunk?) backed by a one-shot memoizinghk-forcethat evaluates the deferred AST, then flips aforcedflag and caches the value on the thunk dict; the sharedhk-deep-forcewalks the result tree at the test/output boundary. Three single-line wiring changes in the evaluator make every application argument lazy::appnow wraps its argument inhk-mk-thunkrather than evaluating it. To preserve correctness where values must be inspected,hk-apply,hk-eval-op,hk-eval-if,hk-eval-case, andhk-evalfor:negnow force their operand.hk-apply-builtinforces every collected arg before invoking the underlying SX fn so built-ins (error,not,id) stay strict. The pattern matcher inmatch.sxnow forces the scrutinee just-in-time only for patterns that need to inspect shape —p-wild,p-var,p-as, andp-lazyare no-force paths, so the value flows through as a thunk and binding preserves laziness.hk-match-list-patforces at every cons-spine step. 6 new lazy-specific tests inlib/haskell/tests/eval.sxverify that(\x y -> x) 1 (error …)and(\x y -> y) (error …) 99return without diverging, thatcase Just (error …) of Just _ -> 7short-circuits, thatconstdrops its second arg, thatmyHead (1 : error … : [])returns 1 without touching the tail, and thatJust (error …)survives a wildcard-armcase. 333/333 green, all prior eval tests preserved by deep-forcing the result inhk-eval-expr-sourceandhk-prog-val. -
2026-04-24 — Phase 2 evaluator (
lib/haskell/eval.sx) — ties the whole pipeline together. Strict semantics throughout (laziness is Phase 3). Function values are tagged dicts:closure,multi(fun),con-partial,builtin.hk-applyunifies dispatch across all four; closures and multifuns curry one argument at a time, multifuns trying each clause's pat-list in order once arity is reached. Top-levelhk-bind-decls!is three-pass — collect groups + pre-seed names → install multifuns (so closures observe later names) → eval 0-arity bodies and pat-binds — making forward and mutually recursive references work.hk-eval-letdoes the same trick with a mutable child env. Built-ins:error/not/id, plusotherwise = True. Operators wired: arithmetic, comparison (returning Bool conses),&&,||,:,++. Sections evaluate the captured operand once and return a closure synthesized via the existing AST.hk-eval-programregisters data decls then binds, returning the env;hk-runfetchesmainif present. Also extendedruntime.sxto pre-register the standard Prelude conses (Maybe,Either,Ordering) so expression-level eval doesn't need a leadingdatadecl. 48 new tests inlib/haskell/tests/eval.sxcover literals, arithmetic precedence, comparison/Bool,if,let(incl. recursive factorial), lambdas (incl. constructor pattern args), constructors,case(Just/Nothing/literal/tuple/wildcard), list literals + cons +++, tuples, sections, multi-clause top-level (factorial, list length via cons pattern, Maybe handler with default), user-defineddatawith case-style matching, a binary-tree height program, currying, higher-order (twice), short-circuiterrorviaif, and the three built-ins. 329/329 green. Phase 2 is now complete; Phase 3 (laziness) is next. -
2026-04-24 — Phase 2: value-level pattern matcher (
lib/haskell/match.sx). Core entryhk-match pat val envreturns an extended env dict on success ornilon failure (usesassocrather thandict-set!so failed branches never pollute the caller's env). Constructor values are tagged lists with the constructor name as the first element; tuples use the tag"Tuple", lists are chained(":" h t)cons cells terminated by("[]"). Value buildershk-mk-con/hk-mk-tuple/hk-mk-nil/hk-mk-cons/hk-mk-listkeep tests readable. The matcher handles every pattern node the parser emits::p-wild(always matches),:p-var(binds),:p-int/:p-float/:p-string/:p-char(literal equality):p-as(sub-match then bind whole),:p-lazy(eager for now; laziness wired in phase 3):p-conwith arity check + recursive arg matching, including deeply nested patterns and infix:cons (uses the same code path as named constructors):p-tupleagainst"Tuple"values,:p-listagainst an exact-length cons spine. Helperhk-parse-pat-sourcelifts a real Haskell pattern out ofcase _ of <pat> -> 0, letting tests drive against parser output. 31 new tests inlib/haskell/tests/match.sxcover atomic patterns, success/failure for each con/tuple/list shape, nestedJust (Just x), cons-vs-empty,asover con / wildcard / failing-sub,~lazy, plus four parser-driven cases (Just x,x : xs,(a, b),n@(Just x)). 281/281 green.
-
2026-04-24 — Phase 2: runtime constructor registry (
lib/haskell/runtime.sx). A mutable dicthk-constructorskeyed by constructor name, each entry carrying arity and owning type.hk-register-data!walks a:dataAST and registers every:con-defwith its arity (= number of field types) and the type name;hk-register-newtype!does the one-constructor variant;hk-register-decls!/hk-register-program!filter a decls list (or a:program/:moduleAST) and call the appropriate registrar.hk-load-source!composes it withhk-core(tokenize → layout → parse → desugar → register). Pre-registers five built-ins tied to Haskell syntactic forms:True/False(Bool),[]and:(List),()(Unit) — everything else comes from user declarations or the eventual Prelude. Query helpers:hk-is-con?,hk-con-arity,hk-con-type,hk-con-names. 24 new tests inlib/haskell/tests/runtime.sxcover each built-in (arity + type), unknown-name probes, registration ofMyBool/Maybe/Either/ recursiveTree/newtype Age, multi-data programs, a module-header body, ignoring non-data decls, and last-wins re-registration. 250/250 green. -
2026-04-24 — Phase 2 kicks off with
lib/haskell/desugar.sx— a tree-walking rewriter that eliminates the three surface-only forms produced by the parser, leaving a smaller core AST for the evaluator::where BODY DECLS→:let DECLS BODY:guarded ((:guard C1 E1) (:guard C2 E2) …)→ right-folded(:if C1 E1 (:if C2 E2 … (:app (:var "error") (:string "…")))):list-comp E QUALS→ Haskell 98 §3.11 translation: empty quals →(:list (E)),:q-guard→(:if … (:list (E)) (:list ())),:q-gen PAT SRC→(concatMap (\PAT -> …) SRC),:q-let BINDS→(:let BINDS …). Nested generators compile to nested concatMap. Every other expression, decl, pattern, and type node is recursed into and passed through unchanged. Public entrieshk-desugar,hk-core(tokenize → layout → parse → desugar on a module), andhk-core-expr(the same for an expression). 15 new tests inlib/haskell/tests/desugar.sxcover two- and three-way guards, case-alt guards, single/multi-bindingwhere, guards +wherecombined, the four list-comprehension cases (single-gen, gen + filter, gen + let, nested gens), and pass-through for literals, lambdas, simple fun-clauses,datadecls, and a module header wrapping a guarded function. 226/226 green.
-
2026-04-24 — Phase 1 parser is now complete. This iteration adds operator sections and list comprehensions, the two remaining aexp-level forms, plus ticks the “AST design” item (the keyword- tagged list shape has accumulated a full HsSyn-level surface). Changes:
hk-parse-infixnow bails onop )without consuming the op, so the paren parser can claim it as a left section.hk-parse-parensrewritten to recognise five new forms:()(unit),(op)→(:var OP),(op e)→(:sect-right OP E)(excluded for-so that(- 5)stays(:neg 5)),(e op)→(:sect-left OP E), plus regular parens and tuples. Works for varsym, consym, reservedop:, and backtick-quoted varids.hk-section-op-infoinspects the current token and returns a{:name :len}dict, so the same logic handles 1-token ops and 3-token backtick ops uniformly.hk-parse-list-litnow recognises a|after the first element and dispatches tohk-parse-qualper qualifier (comma-separated), producing(:list-comp EXPR QUALS). Qualifiers are:(:q-gen PAT EXPR)when a paren-balanced lookahead (hk-comp-qual-is-gen?) finds<-before the next,/],(:q-let BINDS)forlet …, and(:q-guard EXPR)otherwise.hk-parse-comp-letaccepts]or,as an implicit block close (single-line comprehensions never see layout's vrbrace before the qualifier terminator arrives); explicit{ }still closes strictly. 22 new tests inlib/haskell/tests/parser-sect-comp.sxcover op-references (inc.(-),(:), backtick), right sections (inc. backtick), left sections, the(- 5)→:negcorner, plain parens and tuples, six comprehension shapes (simple, filter, let, nested-generators, constructor pattern bind, tuple pattern bind, and a three-qualifier mix). 211/211 green.
-
2026-04-24 — Phase 1: module header + imports. Added
hk-parse-module-header,hk-parse-import, plus shared helpers for import/export entity lists (hk-parse-ent,hk-parse-ent-member,hk-parse-ent-list). New AST:(:module NAME EXPORTS IMPORTS DECLS)— NAMEnilmeans no header, EXPORTSnilmeans no export list (distinct from empty())(:import QUALIFIED NAME AS SPEC)— QUALIFIED bool, AS alias or nil, SPEC nil /(:spec-items ENTS)/(:spec-hiding ENTS)- Entity refs:
:ent-var,:ent-all(Tycon(..)),:ent-with(Tycon(m1, m2, …)),:ent-module(exports only).hk-parse-programnow dispatches on the leading token:modulekeyword → full header-plus-body parse (consuming thewherelayout brace around the module body); otherwise collect any leadingimportdecls and then remaining decls with the existing logic. The outer shell is(:module …)as soon as any header or import is present, and stays as(:program DECLS)otherwise — preserving every previous test expectation untouched. Handles operator exports((+:)), dotted module names (Data.Map), and the Haskell-98 context-sensitive keywordsqualified/as/hiding(all lexed as ordinary varids and matched only in import position). 16 new tests inlib/haskell/tests/parser-module.sxcovering simple/exports/empty headers, dotted names, operator exports,module Fooexports, qualified/aliased/items/hiding imports, and a headerless-with-imports file. 189/189 green.
-
2026-04-24 — Phase 1: guards + where clauses. Factored a single
hk-parse-rhs septhat all body-producing sites now share: it reads a plainsep exprbody or a chain of| cond sep exprguards, then — regardless of which form — looks for an optionalwhereblock and wraps accordingly. AST additions::guarded GUARDSwhere each GUARD is:guard COND EXPR:where BODY DECLSwhere BODY is a plain expr or a:guardedBoth can nest (guards inside where).hk-parse-altnow routes throughhk-parse-rhs "->",hk-parse-fun-clauseandhk-parse-bindthroughhk-parse-rhs "=".hk-parse-where-declsreuseshk-parse-declso where-blocks accept any decl form (signatures, fixity, nested funs). As a side effect,hk-parse-bindnow also picks up the Haskell-nativelet f x = …funclause shorthand: a varid followed by one or more apats produces(:fun-clause NAME APATS BODY)instead of a(:bind (:p-var …) …)— keeping the simplelet x = eshape unchanged for existing tests. 11 new tests inlib/haskell/tests/parser-guards-where.sxcover two- and three-way guards, mixed guarded + equality clauses, single- and multi-binding where blocks, guards plus where, case-alt guards, case-alt where, let with funclause shorthand, let with guards, and a where containing a type signature alongside a fun-clause. 173/173 green.
-
2026-04-24 — Phase 1: top-level decls. Refactored
hk-parse-exprinto ahk-parser tokens modewith:expr/:moduledispatch so the big lexical state is shared (peek/advance/pat/expr helpers all reachable); added public wrappershk-parse-expr,hk-parse-module, and source-level entryhk-parse-top. New type parser (hk-parse-type/hk-parse-btype/hk-parse-atype): type variables (:t-var), type constructors (:t-con), type application (:t-app, left-assoc), right-associative function arrow (:t-fun), unit/tuples (:t-tuple), and lists (:t-list). New decl parser (hk-parse-decl/hk-parse-program) producing a(:program DECLS)shell::type-sig NAMES TYPE— comma-separated multi-name support:fun-clause NAME APATS BODY— patterns for args, body via existing expr:pat-bind PAT BODY— top-level pattern bindings like(a, b) = pair:data NAME TVARS CONSwith:con-def CNAME FIELDSfor nullary and multi-arg constructors, including recursive references:type-syn NAME TVARS TYPE,:newtype NAME TVARS CNAME FIELD:fixity ASSOC PREC OPS— assoc one of"l"/"r"/"n", default prec 9, comma-separated operator names, including backtick-quoted varids. Sig vs fun-clause disambiguated by a paren-balanced top-level scan for::before the next;/}(hk-has-top-dcolon?). 24 new tests inlib/haskell/tests/parser-decls.sxcover all decl forms, signatures with application / tuples / lists / right-assoc arrows, nullary and recursive data types, multi-clause functions, and a mixed program with data + type- synonym + signature + two function clauses. Not yet: guards, where clauses, module header, imports, deriving, contexts, GADTs. 162/162 green.
-
2026-04-24 — Phase 1: full patterns. Added
aspatterns (name@apat→(:p-as NAME PAT)), lazy patterns (~apat→(:p-lazy PAT)), negative literal patterns (-N/-Fresolving eagerly in the parser so downstream passes see a plain(:p-int -1)), and infix constructor patterns via a right-associative single-band layer on top ofhk-parse-pat-lhsfor anyconsymor reservedop:(sox : xsparses as(:p-con ":" [x, xs]),a :+: blikewise). Extendedhk-apat-start?with-and~so the pattern-argument loops in lambdas and constructor applications pick these up. Lambdas now parse apat parameters instead of bare varids — so the:lambdaAST is(:lambda APATS BODY)with apats as pattern nodes.hk-parse-bindbecame a plainpat = exprform, so:bindnow has a pattern LHS throughout (simplex = 1→(:bind (:p-var "x") …)); this picks uplet (x, y) = pair in …andlet Just x = m in xautomatically, and flows throughdo-notation lets. Eight existing tests updated to the pattern-flavoured AST. Also fixed a pragmatic layout issue that surfaced in multi-linelets: when a layout-indent would emit a spurious;just before anintoken (because the let block had already been closed by dedent),hk-peek-next-reservednow lets the layout pass skip that indent and leave closing to the existinginhandler. 18 new tests inlib/haskell/tests/parser-patterns.sxcover every pattern variant, lambda with mixed apats, let pattern-bindings (tuple / constructor / cons), and do-bind with a tuple pattern. 138/138 green. -
2026-04-24 — Phase 1:
case … ofanddo-notation parsers. Addedhk-parse-case/hk-parse-alt,hk-parse-do/hk-parse-do-stmt/hk-parse-do-let, plus the minimal pattern language needed to make arms and binds meaningful:hk-parse-apat(var, wildcard_, int/float/string/char literal, 0-arity conid/qconid, paren+tuple, list) andhk-parse-pat(conid applied to apats greedily). AST nodes::case SCRUT ALTS,:alt PAT BODY,:do STMTSwith stmts:do-expr E/:do-bind PAT E/:do-let BINDS, and pattern tags:p-wild/:p-int/:p-float/:p-string/:p-char/:p-var/:p-con NAME ARGS/:p-tuple/:p-list.do-stmts disambiguatepat <- evs bare expression with a forward paren/bracket/brace-balanced scan for<-before the next;/}— no backtracking, no AST rewrite.caseanddoaccept both implicit (vlbrace/vsemi/vrbrace) and explicit braces. Added tohk-parse-lexpso they participate fully in operator-precedence expressions. 19 new tests inlib/haskell/tests/parser-case-do.sxcover every pattern variant, explicit-bracecase, expression scrutinees, do with bind/let/expr, multi-bindingletindo, constructor patterns in binds, andcase/donested insideletand lambda. The full pattern item (as patterns, negative literals,~lazy, lambda/let pattern extension) remains a separate sub-item. 119/119 green. -
2026-04-24 — Phase 1: expression parser (
lib/haskell/parser.sx, ~380 lines). Pratt-style precedence climbing against a Haskell-98-default op table (24 operators across precedence 0–9, left/right/non assoc, default infixl 9 for anything unlisted). Supports literals (int/float/string/char), varid/conid (qualified variants folded into:var/:con), parens / unit / tuples, list literals, ranges[a..b]and[a,b..c], left-associative application, unary-, backtick operators (x \mod` 3), lambdas,if-then-else, andlet … inconsuming both virtual and explicit braces. AST uses keyword tags (:var,:op,:lambda,:let,:bind,:tuple,:range,:range-step,:app,:neg,:if,:list,:int,:float,:string,:char,:con). The parser skips a leadingvlbrace/lbraceso it can be called on full post-layout output, and uses araise-based error channel with location-lite messages. 42 new tests inlib/haskell/tests/parser-expr.sxcover literals, identifiers, parens/tuple/unit, list + range, app associativity, operator precedence (mul over add, cons right-assoc, function-composition right-assoc,$lowest), backtick ops, unary-, lambda multi-param,ifwith infix condition, single- and multi-bindinglet` (both implicit and explicit braces), plus a few mixed nestings. 100/100 green. -
2026-04-24 — Phase 1: layout algorithm (
lib/haskell/layout.sx, ~260 lines) implementing Haskell 98 §10.3. Two-pass design: a pre-pass augments the raw token stream with explicitlayout-open/layout-indentmarkers (suppressing<n>when{n}already applies, per note 3), then an L pass consumes the augmented stream against a stack of implicit/explicit layout contexts and emitsvlbrace/vsemi/vrbracetokens; newlines are dropped. Supports the initial module-level implicit open (skipped when the first token ismoduleor{), the four layout keywords (let/where/do/of), explicit braces disabling layout, dedent closing nested implicit blocks while also emittingvsemiat the enclosing level, and the pragmatic single-linelet … inrule (emit}wheninmeets an implicit let). 15 new tests inlib/haskell/tests/layout.sxcover module-start, do/let/where/case/of, explicit braces, multi-level dedent, line continuation, and EOF close-down. Shared test helpers moved tolib/haskell/testlib.sxso both test files can share onehk-test.test.shpreloads tokenizer + layout + testlib. 58/58 green. -
2026-04-24 — Phase 1: Haskell 98 tokenizer (
lib/haskell/tokenizer.sx, 490 lines) covering idents (lower/upper/qvarid/qconid), 23 reserved words, 11 reserved ops, varsym/consym operator chains, integer/hex/octal/float literals incl. exponent notation, char + string literals with escape sequences, nested{- ... -}block comments with depth counter,-- ... EOLline comments (respecting the "followed by symbol = not a comment" Haskell 98 rule), backticks, punctuation, and explicitnewlinetokens for the upcoming layout pass. 43 structural tests inlib/haskell/tests/parse.sx, a lightweighthk-deep=?equality helper and a customlib/haskell/test.shrunner (pipes through the OCaml epoch protocol, falls back to the main-repo build when run from a worktree). 43/43 green.Also peeked at
/root/rose-ash/sx-haskell/per briefing: that directory is a Haskell program implementing an SX interpreter (Types.hs, Eval.hs, Primitives.hs, etc. — ~2800 lines of .hs) — the opposite direction from this project. Nothing to fold in.Gotchas hit:
emit!andpeekare SX evaluator special forms, so every local helper uses thehk-prefix.cond/when/letclauses evaluate ONLY the last expression; multi-expression bodies MUST be wrapped in(do ...). These two together account for all the tokenizer's early crashes.
Blockers
- (none yet)