diff --git a/lib/haskell/desugar.sx b/lib/haskell/desugar.sx index c2b5ebdc..17d30ca4 100644 --- a/lib/haskell/desugar.sx +++ b/lib/haskell/desugar.sx @@ -210,6 +210,7 @@ :op (nth node 1) (hk-desugar (nth node 2)) (hk-desugar (nth node 3)))) + ((= tag "type-ann") (hk-desugar (nth node 1))) ((= tag "neg") (list :neg (hk-desugar (nth node 1)))) ((= tag "if") (list diff --git a/lib/haskell/parser.sx b/lib/haskell/parser.sx index 3642d979..97f7884f 100644 --- a/lib/haskell/parser.sx +++ b/lib/haskell/parser.sx @@ -275,38 +275,47 @@ (list :sect-right op-name expr-e)))))) (:else (let - ((first-e (hk-parse-expr-inner)) - (items (list)) - (is-tuple false)) - (append! items first-e) - (define - hk-tup-loop - (fn - () - (when - (hk-match? "comma" nil) - (do - (hk-advance!) - (set! is-tuple true) - (append! items (hk-parse-expr-inner)) - (hk-tup-loop))))) - (hk-tup-loop) + ((first-e (hk-parse-expr-inner))) (cond - ((hk-match? "rparen" nil) + ((hk-match? "reservedop" "::") (do (hk-advance!) - (if is-tuple (list :tuple items) first-e))) + (let + ((ann-type (hk-parse-type))) + (hk-expect! "rparen" nil) + (list :type-ann first-e ann-type)))) (:else (let - ((op-info2 (hk-section-op-info))) + ((items (list)) (is-tuple false)) + (append! items first-e) + (define + hk-tup-loop + (fn + () + (when + (hk-match? "comma" nil) + (do + (hk-advance!) + (set! is-tuple true) + (append! items (hk-parse-expr-inner)) + (hk-tup-loop))))) + (hk-tup-loop) (cond - ((and (not (nil? op-info2)) (not is-tuple) (let ((after2 (hk-peek-at (get op-info2 "len")))) (and (not (nil? after2)) (= (get after2 "type") "rparen")))) - (let - ((op-name (get op-info2 "name"))) - (hk-consume-op!) + ((hk-match? "rparen" nil) + (do (hk-advance!) - (list :sect-left op-name first-e))) - (:else (hk-err "expected ')' after expression")))))))))))))) + (if is-tuple (list :tuple items) first-e))) + (:else + (let + ((op-info2 (hk-section-op-info))) + (cond + ((and (not (nil? op-info2)) (not is-tuple) (let ((after2 (hk-peek-at (get op-info2 "len")))) (and (not (nil? after2)) (= (get after2 "type") "rparen")))) + (let + ((op-name (get op-info2 "name"))) + (hk-consume-op!) + (hk-advance!) + (list :sect-left op-name first-e))) + (:else (hk-err "expected ')' after expression"))))))))))))))))) (define hk-comp-qual-is-gen? (fn @@ -1724,10 +1733,18 @@ (= (hk-peek-type) "eof") (hk-match? "vrbrace" nil) (hk-match? "rbrace" nil)))) + (define + hk-body-step + (fn + () + (cond + ((hk-match? "reserved" "import") + (append! imports (hk-parse-import))) + (:else (append! decls (hk-parse-decl)))))) (when (not (hk-body-at-end?)) (do - (append! decls (hk-parse-decl)) + (hk-body-step) (define hk-body-loop (fn @@ -1738,7 +1755,7 @@ (hk-advance!) (when (not (hk-body-at-end?)) - (append! decls (hk-parse-decl))) + (hk-body-step)) (hk-body-loop))))) (hk-body-loop))) (list imports decls)))) diff --git a/lib/haskell/tests/parse-extras.sx b/lib/haskell/tests/parse-extras.sx new file mode 100644 index 00000000..849f2217 --- /dev/null +++ b/lib/haskell/tests/parse-extras.sx @@ -0,0 +1,102 @@ +;; Phase 17 — parser polish unit tests. + +(hk-test + "type-ann: literal int annotated" + (hk-deep-force (hk-run "main = (42 :: Int)")) + 42) + +(hk-test + "type-ann: arithmetic annotated" + (hk-deep-force (hk-run "main = (1 + 2 :: Int)")) + 3) + +(hk-test + "type-ann: function arg annotated" + (hk-deep-force + (hk-run "f x = x + 1\nmain = f (1 :: Int)")) + 2) + +(hk-test + "type-ann: string annotated" + (hk-deep-force (hk-run "main = (\"hi\" :: String)")) + "hi") + +(hk-test + "type-ann: bool annotated" + (hk-deep-force (hk-run "main = (True :: Bool)")) + (list "True")) + +(hk-test + "type-ann: tuple annotated" + (hk-deep-force (hk-run "main = ((1, 2) :: (Int, Int))")) + (list "Tuple" 1 2)) + +(hk-test + "type-ann: nested annotation in arithmetic" + (hk-deep-force (hk-run "main = (1 :: Int) + (2 :: Int)")) + 3) + +(hk-test + "type-ann: function-typed annotation passes through eval" + (hk-deep-force + (hk-run "main = let f = ((\\x -> x + 1) :: Int -> Int) in f 5")) + 6) + +(hk-test + "no regression: plain parens still work" + (hk-deep-force (hk-run "main = (5)")) + 5) + +(hk-test + "no regression: 3-tuple still works" + (hk-deep-force (hk-run "main = (1, 2, 3)")) + (list "Tuple" 1 2 3)) + +(hk-test + "no regression: section-left still works" + (hk-deep-force (hk-run "main = (3 +) 4")) + 7) + +(hk-test + "no regression: section-right still works" + (hk-deep-force (hk-run "main = (+ 3) 4")) + 7) + +(hk-test + "import: still works as the very first decl" + (hk-deep-force + (hk-run "import qualified Data.IORef as I +main = do { r <- I.newIORef 7; I.readIORef r }")) + (list "IO" 7)) + +(hk-test + "import: between decls — after main" + (hk-deep-force + (hk-run "main = do { r <- I.newIORef 11; I.readIORef r } +import qualified Data.IORef as I")) + (list "IO" 11)) + +(hk-test + "import: between two decls — uses helper after import" + (hk-deep-force + (hk-run "f x = x + 100 +import qualified Data.IORef as I +main = do { r <- I.newIORef 5; I.modifyIORef r f; I.readIORef r }")) + (list "IO" 105)) + +(hk-test + "import: two imports in different positions" + (hk-deep-force + (hk-run "import qualified Data.IORef as I +helper x = x * 2 +import qualified Data.Map as M +main = do { r <- I.newIORef (helper 21); I.readIORef r }")) + (list "IO" 42)) + +(hk-test + "import: unqualified, mid-file" + (hk-deep-force + (hk-run "go x = x +import Data.IORef +main = go 9")) + 9) diff --git a/lib/haskell/tests/typecheck.sx b/lib/haskell/tests/typecheck.sx index 6f46e089..242e476e 100644 --- a/lib/haskell/tests/typecheck.sx +++ b/lib/haskell/tests/typecheck.sx @@ -16,15 +16,18 @@ true))) ;; ─── Valid programs pass through ───────────────────────────────────────────── -(hk-test "typed ok: simple arithmetic" (hk-run-typed "main = 1 + 2") 3) +(hk-test "typed ok: simple arithmetic" + (hk-deep-force (hk-run-typed "main = 1 + 2")) 3) -(hk-test "typed ok: boolean" (hk-run-typed "main = True") (list "True")) +(hk-test "typed ok: boolean" + (hk-deep-force (hk-run-typed "main = True")) (list "True")) -(hk-test "typed ok: let binding" (hk-run-typed "main = let x = 1 in x + 2") 3) +(hk-test "typed ok: let binding" + (hk-deep-force (hk-run-typed "main = let x = 1 in x + 2")) 3) (hk-test "typed ok: two independent fns" - (hk-run-typed "f x = x + 1\nmain = f 5") + (hk-deep-force (hk-run-typed "f x = x + 1\nmain = f 5")) 6) ;; ─── Untypeable programs are rejected ──────────────────────────────────────── @@ -76,7 +79,7 @@ (hk-test "run-typed sig ok: Int declared matches" - (hk-run-typed "main :: Int\nmain = 1 + 2") + (hk-deep-force (hk-run-typed "main :: Int\nmain = 1 + 2")) 3) {:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail} \ No newline at end of file diff --git a/plans/haskell-completeness.md b/plans/haskell-completeness.md index c2ae9398..dd4f56e0 100644 --- a/plans/haskell-completeness.md +++ b/plans/haskell-completeness.md @@ -316,11 +316,11 @@ No OCaml changes are needed. The view type is fully representable as an SX dict. Real Haskell programs use these on every page; closing the gaps unblocks larger conformance programs and removes one-line workarounds in test sources. -- [ ] Type annotations in expressions: `(x :: Int)`, `f (1 :: Int)`, +- [x] Type annotations in expressions: `(x :: Int)`, `f (1 :: Int)`, `return (42 :: Int)`. Parser currently rejects `::` in `aexp` position; desugar should drop the annotation (we have no inference at this layer yet, so it's a parse-only pass-through). -- [ ] `import` declarations anywhere at the start of a module — currently +- [x] `import` declarations anywhere at the start of a module — currently only the very-top-of-file form is recognised. Real test programs that mix prelude code with `import qualified Data.IORef` need this. - [ ] Multi-line top-level `where` blocks (`where { ... }` with explicit @@ -359,10 +359,100 @@ that to single-digit minutes. - [ ] Verify the scoreboard output is byte-identical to the old per-process driver, then keep the per-process path as `--isolated` for debugging. +### Phase 20 — Close Algorithm W gaps + +`lib/haskell/infer.sx` already implements core HM (TVar/TCon/TArr/TApp/TTuple/ +TScheme, substitution, occurs-check unification, instantiate/generalize, let- +polymorphism). 75 inference unit tests + 15 typecheck integration tests pass. +The remaining gaps that block typing real programs: + +- [ ] `case` expressions in `hk-w`. Needs to infer scrutinee type, then for + each `(:alt pat body)` infer the pattern's binding env (extending + `hk-w-pat`) and unify body types across alts. +- [ ] `do` notation: extend `hk-type-env0` with `return :: a -> IO a`, + `(>>=) :: IO a -> (a -> IO b) -> IO b`, `(>>) :: IO a -> IO b -> IO b`, + and primitive IO actions (`putStrLn :: String -> IO ()`, + `getLine :: IO String`, etc.). May need a `TApp (TCon "IO") a` shape. +- [ ] Record-accessor desugaring leaves `__rec_field` placeholder visible to + inference. Either skip generated accessor clauses during `hk-infer-prog` + or rewrite the desugar to produce a typed shape. +- [ ] Type annotations in expressions `(x :: Int)` (parser also needed; see + Phase 17). Infer should unify the inferred type with the annotation. +- [ ] Tests in `lib/haskell/tests/infer-extras.sx` (≥ 10) covering the + above shapes. + +### Phase 21 — Type classes (Eq, Ord, Num, Show) + +The evaluator already implements typeclass dispatch via dict-passing +(`__default__ClassName_method` + per-instance dicts). The type system +ignores `class` and `instance` decls. Closing this means HM with +constraints (qualified types `[ClassName var] => type`). + +- [ ] Extend the type representation: `(TQual CONSTRAINTS TYPE)` where + `CONSTRAINTS = [(class-name . type-arg), …]`. +- [ ] Generalize → `forall vars. preds => type`; instantiate → fresh-rename + vars in both preds and type. +- [ ] During inference, when a primitive operator that needs a class is + used (e.g. `+`), emit a constraint `(Num t)`; collect constraints in + the substitution-threading. +- [ ] At let-generalization, simplify constraints (defaulting for `Num` + literals → `Int`; entailment via known instances). +- [ ] `class` declarations register members with their qualified type; + `instance` declarations register a witness. +- [ ] At top-level, if any unsolvable constraint remains → type error + ("No instance for X"). +- [ ] Tests in `lib/haskell/tests/typeclasses.sx` (≥ 12 covering Eq, Ord, + Num overloading, show on instances, instance ambiguity rejection). + +### Phase 22 — Typecheck-then-run as the default + +- [ ] Replace `hk-run` with a typecheck-first variant in the conformance + driver, or run conformance twice (once typed, once untyped) and report + both pass-rates in `scoreboard.md`. +- [ ] Investigate which existing 36 programs are untypeable due to gaps + closed in Phase 20-21 vs genuinely dynamically-typed; aim for ≥ 30/36 + programs typechecking before committing to the swap. +- [ ] If swap is committed, retire `hk-run` callsites in tests in favour + of `hk-run-typed`; keep the untyped path available for parser/eval + development against in-progress features. + ## Progress log _Newest first._ +**2026-05-10** — Phase 17 second box: `import` declarations anywhere among +top-level decls. `hk-collect-module-body` previously ran a fixed +import-loop at the start, then a separate decl-loop; merged into a single +`hk-body-step` dispatcher that routes `import` to the imports list and +everything else to `hk-parse-decl`. Each call site (initial step + post- +semicolon loop) now uses the dispatcher. Imports collected mid-stream +still feed into `hk-bind-decls!` correctly because the eval side reads +them via the imports list, not by AST position. tests/parse-extras.sx +12 → 17 covering very-top, mid-stream, post-main, two-imports-different- +positions, and unqualified mid-file. Regression: eval 66/0, exceptions +14/0, typecheck 15/0, records 14/0, ioref 13/0, map 26/0, set 17/0. + +**2026-05-08** — Phase 17 first box: expression type annotations `(x :: Int)`, +`f (1 :: Int)`, `(\x -> x+1) :: Int -> Int`. Parser's `hk-parse-parens` +gains a `::` arm after the first inner expression: consume `::`, parse a +type via the existing `hk-parse-type`, expect `)`, emit `(:type-ann EXPR +TYPE)`. Desugar drops the annotation — `:type-ann E _ → (hk-desugar E)` — +since the existing eval path has no type-directed dispatch; Phase 20 will +let inference consume the annotation. tests/parse-extras.sx 12/12; eval, +exceptions, typecheck, records, ioref still clean. + +**2026-05-08** — Plan extends with Phases 20-22 (HM type system). Discovered +during planning that `lib/haskell/infer.sx` already lands core Algorithm W +(75 inference unit tests pass; let-polymorphism, sig checking, error +reporting via `hk-expr->brief`). Fixed five regressing tests in +`lib/haskell/tests/typecheck.sx` that compared an unforced thunk against +the expected value — added `hk-deep-force` around `hk-run-typed` to match +the existing untyped-path convention. typecheck.sx now 15/15. +Phase 20 captures the remaining Algorithm W gaps (case, do, record +accessors, expression annotations); Phase 21 captures type classes with +qualified types; Phase 22 captures the integration step (typecheck-then-run +across conformance). + **2026-05-08** — Phase 16 Exception handling complete (6 ops + module wiring + 14 unit tests + 2 conformance programs). `hk-bind-exceptions!` in `eval.sx` registers `throwIO`, `throw`, `evaluate`, `catch`, `try`, `handle`, and