Skip to content

feat: add Haskell language support - #1561

Open
mightybyte wants to merge 3 commits into
colbymchenry:mainfrom
mightybyte:feat/haskell-language-support
Open

feat: add Haskell language support#1561
mightybyte wants to merge 3 commits into
colbymchenry:mainfrom
mightybyte:feat/haskell-language-support

Conversation

@mightybyte

Copy link
Copy Markdown

Add full Haskell (.hs, .lhs) support to CodeGraph's extraction pipeline:

  • Functions with multi-clause grouping, type signatures, and Haddock docs
  • Type classes as traits, instances with implements edges
  • Algebraic data types as structs, constructors as enum_members, record fields
  • Type synonyms, newtypes
  • Module imports with dotted module names (Data.List, etc.)
  • Call edges: local calls, qualified calls (Mod.fn), data constructor instantiations, and infix operator calls (user-defined operators only)
  • where-clause helper extraction

Grammar: vendored tree-sitter-haskell 0.23.1 wasm (ABI 14) from the npm package — tree-sitter-wasms doesn't ship Haskell.

Validated on three repos by size tier:

  • xmonad/xmonad (small, 41 files, 1222 nodes, 2208 edges)
  • PostgREST/postgrest (medium, 192 files, 5531 nodes, 9386 edges)
  • jgm/pandoc (large, 599 files, 23126 nodes, 52180 edges)

All 630 extraction tests pass (19 new Haskell tests + 611 existing).

Add full Haskell (.hs, .lhs) support to CodeGraph's extraction pipeline:

- Functions with multi-clause grouping, type signatures, and Haddock docs
- Type classes as traits, instances with implements edges
- Algebraic data types as structs, constructors as enum_members, record fields
- Type synonyms, newtypes
- Module imports with dotted module names (Data.List, etc.)
- Call edges: local calls, qualified calls (Mod.fn), data constructor
  instantiations, and infix operator calls (user-defined operators only)
- where-clause helper extraction

Grammar: vendored tree-sitter-haskell 0.23.1 wasm (ABI 14) from the npm
package — tree-sitter-wasms doesn't ship Haskell.

Validated on three repos by size tier:
- xmonad/xmonad (small, 41 files, 1222 nodes, 2208 edges)
- PostgREST/postgrest (medium, 192 files, 5531 nodes, 9386 edges)
- jgm/pandoc (large, 599 files, 23126 nodes, 52180 edges)

All 630 extraction tests pass (19 new Haskell tests + 611 existing).

@codegraph-impact codegraph-impact Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeGraph review

Overall risk: 🟡 Low — New Haskell extractor is fully gated and covered by dedicated tests; the only defect is jargon-heavy Changelog wording that breaks the user-facing rule.

  • ⛔ Violates: Changelog and release contract — The new Changelog entry uses internal graph-model terms (traits, implements edges, enum members, call edges, instantiations) instead of the friendly user-facing prose the rule requires.

@mightybyte — worth a look before this merges.

Verify in the running product (3 checks)

  • cli — Run codegraph init/sync on a repo containing .hs/.lhs files and check codegraph files lists them with language haskell.
  • cli — Use codegraph explore/query on a multi-clause Haskell function (e.g. factorial 0 = 1 / factorial n = ...) and confirm it returns ONE symbol spanning both clauses with the :: Int -> Int signature and Haddock doc, not one symbol per clause.
  • cli — Query codegraph callers/impact for a Haskell function reached only via a built-in operator (e.g. x + y) versus one reached via a user-defined operator, and confirm only the user-defined operator produces a calls edge.

Worth double-checking

  • extractCall Haskell branch and HASKELL_BUILTIN_OPS coverage
  • File-scoped mutable memo for clause merging
What to look for in each
  • extractCall Haskell branch and HASKELL_BUILTIN_OPS coverage — The graph shows no structurally-traced test reaches extractCall's new Haskell branch or HASKELL_BUILTIN_OPS (both flagged NO TEST, 34-36 dependents), because dispatch runs through the generic dynamic visitNode hook. The new extraction.test.ts Haskell suite exercises n * factorial (n - 1) (an infix *) but never asserts that a built-in operator produces NO call edge, nor that a user-defined operator (via prefix_id) produces one. Confirm the new tests actually run in CI and add an assertion for the builtin-exclusion behavior.
  • File-scoped mutable memo for clause merginglastFnFile/lastFnName/lastFnId in haskell.ts are module-level (not per-extractor-instance) variables reset only when resetFnMemo sees a different ctx.filePath. This is documented as safe because 'extraction is file-sequential within a worker' — verify that assumption actually holds in the sync/worker pipeline, since any future concurrent extraction of two files in the same worker would corrupt clause merging across files.
Findings — 1 low
Severity Finding Where
🟡 Low Changelog entry describes internal graph model instead of user-facing behavior (rule: Changelog and release contract) CHANGELOG.md:14 — see inline comment
Business rules — 1 violated · 6 not applicable
Status Rule Note
⛔ Violated Changelog and release contract The new Changelog entry uses internal graph-model terms (traits, implements edges, enum members, call edges, instantiations) instead of the friendly user-facing prose the rule requires.
— Not applicable The graph model The PR doesn't modify the nodes/edges schema; it only creates instances of existing node kinds (function, struct, enum_member, field, trait, class, import, type_alias) via the new Haskell extractor.
— Not applicable Surfaces The change doesn't touch sync/CLI/MCP surface code; watcher.ts's handleChange still calls the pre-existing isSourceFile, unmodified in behavior.
— Not applicable CodeGraph business rules This rule is a meta-description of the business-rules folder itself; the PR doesn't edit that folder's content.
— Not applicable Source strings must exclude interpolated template literals This PR only adds an import and an EXTRACTORS map entry in index.ts; it doesn't touch extractSourceStrings or template-literal handling.
— Not applicable Coverage The quoted language list belongs to a separate product-overview doc that already omits many supported languages; this PR doesn't edit that document.
— Not applicable What is collected The PR doesn't modify telemetry collection code; a new language value flowing through the existing 'languages indexed' counter isn't a change to what/how telemetry collects.
Full assessment

This PR adds Haskell (.hs/.lhs) as a new tree-sitter-backed language: a standalone extractor for functions (with multi-clause merging), ADTs, type classes/instances, imports and calls, plus a Haskell-specific branch inside the shared extractCall method that is fully gated behind this.language === 'haskell' so it cannot regress any other language. It ships with a 263-line dedicated Haskell test suite covering functions, data types, instances, imports and call edges. The one concrete issue is the new CHANGELOG entry, which is written in internal graph-model vocabulary (traits, implements edges, enum members, instantiations) instead of the friendly user-facing prose the changelog rule requires.

Blast radius: 50 files affected beyond the diff · 274 symbols · 25 test files selected

Tests to run:

  • __tests__/exclude-config.test.ts
  • __tests__/extension-mapping.test.ts
  • __tests__/extraction.test.ts
  • __tests__/frameworks.test.ts
  • __tests__/grammar-wasm-bytes.test.ts
  • __tests__/include-config.test.ts
  • __tests__/kernel-ccpp-parity.test.ts
  • __tests__/kernel-csharp-parity.test.ts
  • __tests__/kernel-dart-parity.test.ts
  • __tests__/kernel-grammar-parity.test.ts
  • __tests__/kernel-kotlin-parity.test.ts
  • __tests__/kernel-lua-parity.test.ts
  • __tests__/kernel-php-parity.test.ts
  • __tests__/kernel-r-parity.test.ts
  • __tests__/kernel-ruby-parity.test.ts
  • __tests__/kernel-rustlang-parity.test.ts
  • __tests__/kernel-scaffold.test.ts
  • __tests__/kernel-scala-parity.test.ts
  • __tests__/kernel-swift-parity.test.ts
  • __tests__/kernel-tsjs-parity.test.ts

Full report

Comment thread CHANGELOG.md Outdated

### New Features

- CodeGraph now indexes **Haskell** (`.hs`, `.lhs`) — functions (including multi-clause definitions with type signatures and Haddock docs), type classes as traits, instances with `implements` edges, algebraic data types as structs with constructors as enum members, record fields, type synonyms, newtypes, module imports, local and qualified call edges, data constructor instantiations, and `where`-clause helper extraction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Low — Changelog entry describes internal graph model instead of user-facing behavior

The new CHANGELOG.md line reads: "type classes as traits, instances with implements edges, algebraic data types as structs with constructors as enum members, record fields, ... local and qualified call edges, data constructor instantiations, and where-clause helper extraction." This is internal graph-schema vocabulary (node/edge kinds), not the "friendly prose" a user-facing changelog entry is required to use per the release contract rule — it reads like the README's per-language extractor spec rather than something an end user would parse.

⛔ Violates the business rule Changelog and release contract.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGELOG entry updated

@codegraph-impact codegraph-impact Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeGraph review

Overall risk: 🔴 High — Haskell's clause-merge keys purely on function name, so same-named methods in different type-class instances (e.g. two show implementations) get silently merged into one node.

  • 🔴 Type-class instance methods sharing a name silently merge into one node — src/extraction/languages/haskell.ts:110 — see inline comment

@mightybyte — worth a look before this merges.

Verify in the running product (3 checks)

  • cli — Run codegraph init/sync on a small Haskell project containing two instance declarations that both define a method with the same name (e.g. instance Show Foo where show ... and instance Show Bar where show ...), then use codegraph node/callers to inspect the resulting symbols for that method name.
  • cli — Index a Haskell file with a qualified import and a qualified call (e.g. import qualified Data.Text as T / T.putStrLn "hi") and use codegraph callers/impact on the imported function to confirm the call graph traces it end-to-end.
  • cli — Run codegraph status/codegraph files against a repo containing .hs/.lhs files.

Worth double-checking

  • Clause-merge scoping in handleFunctionLike
  • Test-reachability gap vs. visible test file
  • Vendored grammar binary
What to look for in each
  • Clause-merge scoping in handleFunctionLike — The continuation-clause branch if (name === lastFnName && lastFnId) in src/extraction/languages/haskell.ts only compares the function's name against the globally-remembered last function name/id — it never checks that the enclosing scope (class/instance/top-level) is the same. Confirm this against a file with two instances defining the same method name and check whether the graph produces one node or two.
  • Test-reachability gap vs. visible test file — The graph marks extractCall, TreeSitterExtractor, HASKELL_BUILTIN_OPS, handleFunctionLike, handleDataType, handleClass, handleInstance, and most other new Haskell symbols as 'NO TEST REACHES THIS', despite tests/extraction.test.ts adding a 'Haskell Extraction' describe block that calls extractFromSource against these code paths. Confirm CI actually executes and passes this new test block, and that it's not being skipped/excluded — the discrepancy between the diff's visible tests and the graph's coverage signal should be resolved before trusting the coverage.
  • Vendored grammar binary — src/extraction/wasm/tree-sitter-haskell.wasm is added as a binary with no diffable content. Confirm it is genuinely the claimed tree-sitter-haskell 0.23.1 (ABI 14) build and that grammar-wasm-bytes.test.ts's byte-parity check actually covers this new entry.
Business rules — 2 honored · 5 not applicable
Status Rule Note
✔ Honored Changelog and release contract The new CHANGELOG entry sits under '### New Features' in user-facing prose (language/extension names, no internal file or function names), matching the changelog contract.
✔ Honored Coverage The change extends documented language coverage by adding a per-language extractor under src/extraction/languages/, consistent with the stated architecture, and updates README's language table and count accordingly.
— Not applicable The graph model The diff adds a new extractor that produces already-defined node kinds (function, struct, enum_member, field, type_alias, trait, class, import) but does not modify the nodes/edges schema itself (src/db/schema.sql untouched).
— Not applicable Surfaces sync/watcher behavior is unchanged; only the extension map that feeds isSourceFile grows to include .hs/.lhs, which is a downstream effect, not a modification of the sync surface itself.
— Not applicable CodeGraph business rules This is introductory framing text for the business-rules folder, not a specific behavior the diff modifies.
— Not applicable Source strings must exclude interpolated template literals The diff's change to src/extraction/languages/index.ts only imports and registers haskellExtractor; it does not touch extractSourceStrings or template-literal handling.
— Not applicable What is collected No telemetry collection code is touched; Haskell would simply become another value in the existing 'languages indexed' counter with no change to what's collected.
Full assessment

This PR adds a full new Haskell extractor (functions, ADTs, type classes/instances, newtypes, imports, call edges) behind a vendored tree-sitter grammar, entirely additive and gated by language === 'haskell' checks so no other language's extraction path is touched. The extraction logic uses module-level mutable state to merge multi-clause function definitions into one node, but that merge condition checks only the function's NAME against the last-created function, not its enclosing scope — so two different type-class instances defining a same-named method (a routine Haskell idiom) will be incorrectly collapsed into a single graph node. The code graph also reports zero test reachability into nearly every new Haskell-specific function, even though the diff adds a large test file, which is worth reconciling before merge.

Blast radius: 50 files affected beyond the diff · 274 symbols · 25 test files selected

Tests to run:

  • __tests__/exclude-config.test.ts
  • __tests__/extension-mapping.test.ts
  • __tests__/extraction.test.ts
  • __tests__/frameworks.test.ts
  • __tests__/grammar-wasm-bytes.test.ts
  • __tests__/include-config.test.ts
  • __tests__/kernel-ccpp-parity.test.ts
  • __tests__/kernel-csharp-parity.test.ts
  • __tests__/kernel-dart-parity.test.ts
  • __tests__/kernel-grammar-parity.test.ts
  • __tests__/kernel-kotlin-parity.test.ts
  • __tests__/kernel-lua-parity.test.ts
  • __tests__/kernel-php-parity.test.ts
  • __tests__/kernel-r-parity.test.ts
  • __tests__/kernel-ruby-parity.test.ts
  • __tests__/kernel-rustlang-parity.test.ts
  • __tests__/kernel-scaffold.test.ts
  • __tests__/kernel-scala-parity.test.ts
  • __tests__/kernel-swift-parity.test.ts
  • __tests__/kernel-tsjs-parity.test.ts

Full report

Comment thread src/extraction/languages/haskell.ts Outdated

// Continuation clause: same-name consecutive function — extend the existing
// node and attribute this clause's calls to it.
if (name === lastFnName && lastFnId) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 High — Type-class instance methods sharing a name silently merge into one node

In handleFunctionLike, if (name === lastFnName && lastFnId) { ... } (src/extraction/languages/haskell.ts:110) treats any function/bind with the same name as the previously-created one as a 'continuation clause' and extends that earlier node instead of creating a new one — but lastFnName/lastFnId are plain module-level variables with no scope awareness (class/instance/top-level). Two different instance blocks implementing the same method name (e.g. instance Show Circle where show ... followed later by instance Show Square where show ...) will have the second instance's method body merged into the first instance's node via visitMatch(match, lastFnId, ctx), mis-attributing its calls and extending the wrong node's endLine. This is a very common Haskell pattern and the only instance-related test in the diff exercises a single instance, so it doesn't catch this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just committed an update that should address this.

Type-class instance methods sharing a name (e.g. two 'show' impls in
different 'instance' blocks) were silently merged into one node because
the clause-merge memo keyed only on function name. Track the top of the
node stack (the instance/class/top-level container ID) alongside the
name so continuation-clause merging only fires within the same scope.

Adds a regression test with two 'instance Shape <T> where area ...'
blocks asserting two distinct 'area' nodes, each contained by its own
instance.
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.

1 participant