feat(haskell): add module-aware extraction and resolution#1337
Draft
michelguillaume wants to merge 10 commits into
Draft
feat(haskell): add module-aware extraction and resolution#1337michelguillaume wants to merge 10 commits into
michelguillaume wants to merge 10 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Credit and lineage
This PR continues and supersedes #395 by @ichxorya. The branch intentionally preserves all four original commits (
f441a09,c0ff295,1c52378, and4abb846) with their original authorship, then merges currentmainand layers the additional extraction, resolution, performance, and regression work on top.Thank you to @ichxorya for the grammar integration, initial extractor, HOF/
where/do/operator/GADT work, and the original evaluation data.What changed
.hs, packages the pinned MIT-licensed ABI-14tree-sitter-haskellWASM, and documents a reproducible pinned rebuild/health-check recipe.->as a top-level function.do, lambda, case, recursivelet/where, list-comprehension, and pattern-guard bindings as lexical scope. Local declarations shadow imports without fabricating project-wide edges.ImportQualifiedPost, aliases, explicit lists,Type(..), selected children,hiding, export lists, and restricted/aliased re-exports.Module/Path.hssuffix and importer proximity, while declining ambiguous ties.doblocks no longer rescan all prior statements for every reference.Why
The earlier implementation established useful syntax extraction, but current CodeGraph resolution assumed path-like imports and could not connect Haskell modules reliably. It also allowed false global edges for patterns, higher-order values, local functions, monadic binds, hidden imports, and restricted re-exports. Incremental re-indexing could preserve an edge after only an export header changed.
This version applies Haskell's lexical and module/import/export rules before accepting a unique target, while remaining a syntax-based graph rather than a compiler frontend.
Impact measured on a production Haskell monorepo
Spec.hsare now connected.doblock dropped from about 5.4 s to about 207 ms in the local extraction benchmark (~26× faster).Validation
npm run buildnpm pack --dry-run: Haskell extractor, WASM, and license are included.Deliberate limits
This is static syntax/scope modeling, not GHC typechecking. It understands monadic syntax and lexical binders (
do,<-,let, guards, comprehensions,>>=,>=>,<$>) but does not infer concrete monad types or perform semantic typeclass/instance dispatch. Duplicate modules are handled conservatively, but Cabal component graphs are not interpreted yet. CPP/Template Haskell expansion,.lhs, and.hscare also not modeled.