Skip to content

feat(haskell): add module-aware extraction and resolution#1337

Open
michelguillaume wants to merge 11 commits into
colbymchenry:mainfrom
TontineTrust:feat/haskell-support-clean
Open

feat(haskell): add module-aware extraction and resolution#1337
michelguillaume wants to merge 11 commits into
colbymchenry:mainfrom
TontineTrust:feat/haskell-support-clean

Conversation

@michelguillaume

@michelguillaume michelguillaume commented Jul 17, 2026

Copy link
Copy Markdown

Credit and lineage

This PR continues and supersedes #395 by @ichxorya. The branch intentionally preserves the four original commits (f441a09, c0ff295, 1c52378, and 4abb846) with their original authorship, then merges current main and layers the extraction, resolution, performance, sync-safety, and regression work on top.

Thank you to @ichxorya for the grammar integration, initial extractor, HOF/where/do/operator/GADT work, and original evaluation data.

What changed

Extraction

  • Registers .hs and packages the pinned, MIT-licensed ABI-14 tree-sitter-haskell WASM with a reproducible rebuild/health-check recipe.
  • Extracts modules, grouped equations/signatures, point and pattern bindings, constants/functions, lexical helpers, ADTs/newtypes/GADTs (including grouped constructors), data families/instances, record and pattern-synonym selectors, type aliases/families, classes, instances, standalone deriving, superclasses, and FFI declarations.
  • Handles prefix/infix/backticked/qualified/dotted operators without corrupting operator payloads such as ^., ==., <-., or <.>.
  • Models constructor patterns separately from runtime constructor applications and removes duplicate semantic edges for nullary constructors, sections, and higher-order arguments.
  • Covers do, qualified mdo, rec, lambda/case/view patterns, recursive let/where, list comprehensions, pattern guards, record dot/update syntax, and lexical scopes for same-named helpers.
  • Models executed operands for monadic, applicative, and alternative operators in infix and prefix form (>>=, =<<, >>, <*>, <**>, <|>, *>, <*, <$>, <&>, $>, <$). Continuations remain function references rather than fabricated direct calls.
  • Supports standalone/grouped/bidirectional pattern synonyms, Haddock attachment, bundled pattern exports, builder/matcher dependencies, and top-level destructuring bindings with per-name signatures.

Resolution

  • Resolves Haskell modules from indexed module headers rather than filesystem-like import paths.
  • Supports qualified/unqualified imports, ImportQualifiedPost, aliases, explicit lists, operators, Type(..), selected children, hiding, export lists, and restricted/aliased/wildcard re-exports.
  • Tracks parent ownership for DuplicateRecordFields and propagates ambiguity/restrictions through re-export chains instead of choosing an arbitrary field.
  • Resolves simple annotated OverloadedRecordDot receivers by parent type and deliberately leaves unknown/ambiguous projections unresolved.
  • Resolves local/imported/self-qualified fields, constructors, class selectors, functions, and constants while keeping instance implementations and lexical helpers scoped.
  • Disambiguates duplicate module headers by conventional Module/Path.hs suffix and importer proximity, while declining ties.
  • Gives same-line homonymous lexical declarations stable distinct IDs.

Incremental correctness and performance

  • Replays stamped Haskell import edges atomically when module/import/export topology changes, with a durable recovery marker for interruptions before, during, or after invalidation.
  • Persists a schema-v9 Haskell topology fingerprint per file. Comment/body-only edits re-index and reattach incoming edges without replaying every Haskell import reference; module/import/export/re-export changes retain the conservative global replay.
  • Old indexes with no fingerprint perform one conservative replay and self-heal.
  • Caches signature, export, lexical-binding, and import parsing state. Signature extraction scales linearly in the regression benchmark.

Validation

  • npm run build
  • Focused merged Haskell extraction/resolution/sync matrix: 76/76 passed.
  • Full suite on the final tree: 148 test files passed, 4 skipped; 2,546 tests passed, 49 skipped; 0 failed.
  • Independent extractor and resolver audit agents both reached zero remaining reproducible Haskell P1/P2 findings.
  • Re-export/import fixtures validated with GHC 9.6.7.
  • npm pack --dry-run: 775 package entries; Haskell JS, WASM, schema migration, and grammar license included.
  • git diff --check passed.

Deliberate limits

This remains a static syntax/scope graph, not GHC. It models monadic/applicative syntax, lexical binders, constraints, and import/export visibility, but does not run dictionary elaboration, infer concrete monad stacks, or perform compiler-grade typeclass/instance selection. Cabal component graphs, CPP/Template Haskell expansion, .lhs, and .hsc are not modeled yet; genuinely ambiguous static references are intentionally left unresolved.

The fork keeps a daily/manual upstream merge workflow that validates build, tests, and package contents before updating this branch while the upstream PR remains open.

ichxorya and others added 6 commits May 25, 2026 13:27
Closes three of the gaps flagged after the basic Haskell extractor landed:

- instance C T where ... emits an implements reference from T to C
  (local-receiver lookup; orphan instances where T lives in another file
  remain a known gap).
- data T ... deriving (C1, C2) and the newtype analogue emit one
  implements reference per derived class.
- class (Eq a, Show a) => Ord a where ... emits extends references from
  Ord to Eq and Show. Wired through core extractInheritance with a new
  Haskell context: case alongside the existing extends_clause /
  superclass / base_class_clause arms — no impact on other languages.
- Record-syntax fields (data Foo = Foo { x :: Int }) become field nodes
  scoped to the parent enum. Haskell record selectors live at the type
  level (x :: Foo -> Int), so scoping at the enum, not the constructor,
  is the right model.

Also adds a top-of-file doc block in haskell.ts that enumerates what the
module extracts, what it does NOT extract yet, and how the module is
tested (vitest cases + verify-extraction.mjs on the 4 pinned repos +
agent A/B benchmark).

Verified on 4 pinned repos:
  xmonad     (v0.18.1)    — 799 nodes, 1630 edges, 57 fields, 12 implements
  shellcheck (v0.11.0)    — 2034 / 4137 / 143 fields
  pandoc     (3.9.0.2)    — 16220 / 35985 / 1119 fields / 67 implements / 3 extends
  purescript (c4a35b34)   — 8185 / 15757 / 578 fields / 277 implements

5 new vitest cases (now 13 Haskell tests; 828 tests overall pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends Haskell support with extraction fixes uncovered during an audit
cycle. All changes are additive — 843 tests pass, 0 fails. Real-repo call
coverage roughly doubles to triples across the four pinned repos.

Higher-order call synthesis
  `map area xs` now emits a call edge from the caller to `area`, even
  though `area` is passed as data. The HOF_NAMES allowlist covers ~30
  function-first Foldable / Traversable / list-utility combinators.
  Data-first variants (forM_, for_, forM, for) are deliberately excluded
  — their signature is `t a -> (a -> f b) -> …`, so bridging them would
  emit the data list as a bogus callee.

Where-clause calls
  New `extraBodyFields` hook in LanguageExtractor; used in the core
  extractFunction / extractMethod. Haskell sets it to ['binds'] so the
  framework also walks the `where`-clause sibling field, not just
  `match`. Without this, ~1,900 where-clauses across the four pinned
  repos were dropping their call edges. Shellcheck alone went from
  1,822 → 7,552 calls (+314%) from this fix.

Top-level `bind` extraction
  Tree-sitter parses parameterless top-level bindings (`main = do …`,
  `pi = 3.14`, `constVal = 42`) as `bind` nodes, NOT `function`. Without
  handling, `main` had no node at all — a huge miss for any Haskell
  program. Now emitted as `function` kind.

Operator function definitions
  `x === y = …` (infix style) and `(===) x y = …` (prefix style) had no
  `name:` field — only an `infix` child. Now extracted with the operator
  wrapped in parens (e.g. `function:(===)`).

GADT constructors
  `data Term a where IntT :: Int -> Term Int` uses a `gadt_constructors`
  wrapper instead of `data_constructors`, and each `gadt_constructor`
  carries its `name:` directly (no nested `record`). Both shapes are
  now walked.

Bare-variable do-statements
  `main = do { hi; bye }` — the bare variable in do-statement position
  is the action being run. Tree-sitter wraps it as
  `statement → exp → variable` (no `apply` node), so no call edge was
  emitted. Now `variable`/`constructor` inside a single-child `exp`
  emits a call to the caller.

Scope-stack regression guards
  Custom function / method / operator body walks in visitNode now
  pushScope before visiting body, so calls inside attribute to the
  correct enclosing node. Previously instance-method and operator-
  function calls were leaking to the file scope.

Real-repo call edges (before → after these fixes):
  xmonad       613 →  1,015   (+66%)
  shellcheck 1,822 →  7,552   (+314%)
  pandoc    15,693 → 31,242   (+99%)
  purescript 4,852 → 14,492   (+199%)

Tests
  28 Haskell vitest cases (was 13). 843 total tests pass. Also a 29-case
  audit probe suite with positive and negative coverage (lambdas /
  composed-fns / sections / data-args don't bridge; constructors,
  multi-arg HOFs, case/if/do-block calls, where-bound nested fns,
  do-block bare statements, prefix and infix operator defs, GADT
  constructors, newtype non-stdlib deriving, instance method
  receivers all do).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds on the Haskell support introduced by @ichxorya in colbymchenry#395, whose four commits remain intact in this branch history.
@michelguillaume
michelguillaume marked this pull request as ready for review July 18, 2026 17:07
@develop7

Copy link
Copy Markdown

hey @michelguillaume thank you for picking up the flag! you may want to keep an eye on merge conflicts and have them resolved proactively.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants