Skip to content

fix(haskell): retain arrowless IO-action entry points in the #1312 guard (#2934) - #2968

Merged
squid-protocol merged 1 commit into
mainfrom
fix/2934-haskell-arrowless-io
Sep 11, 2026
Merged

fix(haskell): retain arrowless IO-action entry points in the #1312 guard (#2934)#2968
squid-protocol merged 1 commit into
mainfrom
fix/2934-haskell-arrowless-io

Conversation

@squid-protocol

Copy link
Copy Markdown
Owner

Closes #2934.

Problem

The #1312 point-free guard in _slice_by_indentation (detector.py) skipped every top-level Haskell signature with no arrow token, treating "no ->/" as its whole definition of "point-free value binding". That drops the canonical zero-arg entry point entry :: IO () / main :: IO a — an arrowless signature that does open an executable block under its own name (docs/func_start_rule_contract.md, #2856) — emitting zero FunctionNodes.

Fix (single count/slicer-layer change, per #2812 rule 4)

  • Engine (detector.py): an arrowless signature is skipped only when its return-type head is not IO. New helper _haskell_arrowless_signature_is_action strips a leading forall … . and any … => constraint (mirroring _count_haskell_type_arrows) and whole-token-matches the outermost type-application head, so pure values (Text, Int, IORef Int) still read as CAFs and IORef is never mistaken for IO.
  • Ground truth (tree_sitter_accuracy_audit.py::_get_node_name): mirrored so a bind whose unwrapped signature type is a bare IO … application is kept alongside arrow chains — otherwise the accuracy audit would book the newly-retained entry points as extra_functions false positives.
  • Scope: broader action monads (ReaderT … IO (), m () under MonadIO) are deliberately left to a follow-up; only a bare IO head is retained.

Verification

  • New slicer-level unit tests in test_haskell.py: entry :: IO (), main :: IO a, forall a. IO a, Monad m => IO () all extracted; IORef Int and ReaderT Env IO () stay rejected. Engine and ground-truth mirror agree on all cases.
  • Golden crucible both legs: PASS, zero diff. language-crucible v1.2.0 contains no top-level arrowless IO action (every col-0 IO signature already carries an arrow; the arrowless col-0 signatures are all pure values), so the golden masters legitimately do not move.
  • rosetta: 46/46, 0 regressions — its haskell IORef Int globals correctly stay values.
  • ruff_audit/mypy_audit/dead_key_audit --ci: no new findings. Full pytest tests/: 8739 passed (the only failures are the pre-existing test_security_auditor.py xgboost-env gap, identical on origin/main).

No paired keyword-rosetta corpus rebless is owed — counts do not move on either pinned corpus.

🤖 Generated with Claude Code

…ard (#2934)

The #1312 point-free guard in `_slice_by_indentation` used "no `->`/`⊸`
arrow in the type signature" as its entire test for a point-free VALUE
binding, and dropped every match that failed it. That discards the
canonical zero-arg Haskell entry point `entry :: IO ()` / `main :: IO a`:
an arrowless signature that nonetheless opens an executable block under
its own name (docs/func_start_rule_contract.md, #2856), producing zero
FunctionNodes for files whose only public surface is an arrowless action.

Narrow the guard: an arrowless signature is skipped only when its
return-type head is NOT `IO`. `_haskell_arrowless_signature_is_action`
strips a leading `forall ... .` quantifier and any `... =>` constraint
(mirroring `_count_haskell_type_arrows`'s last-`=>` rule) and matches the
outermost type-application head as a whole token, so pure values
(`Text`, `Int`, `IORef Int`) still read as CAFs -- `IORef` is never
mistaken for `IO`. Broader action monads (`ReaderT ... IO ()`, `m ()`
under a `MonadIO` constraint) are deliberately left to a follow-up.

Mirror the same relaxation on the ground-truth side
(`tree_sitter_accuracy_audit.py::_get_node_name`): a bind whose unwrapped
signature type is a bare `IO ...` application is kept alongside arrow
chains, so the accuracy audit does not book the newly-retained entry
points as extra_functions false positives.

Golden crucible (both legs) is unchanged: language-crucible v1.2.0 has no
top-level arrowless IO action -- every col-0 `IO` signature there already
carries an arrow, and the arrowless col-0 signatures are all pure values.
rosetta stays 46/46 for the same reason (its haskell `IORef Int` globals
stay values). Behaviour is covered by new slicer-level unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXAjGXaZKr2Ge6jjZshPRD
@github-actions

Copy link
Copy Markdown
Contributor

🐦‍⬛ Muninn Security Scan

✅ No security issues found.

🐦‍⬛ Powered by Muninn · Skald Lab

@squid-protocol
squid-protocol marked this pull request as ready for review September 11, 2026 19:20
@squid-protocol
squid-protocol merged commit 4ace17c into main Sep 11, 2026
31 checks passed
@squid-protocol
squid-protocol deleted the fix/2934-haskell-arrowless-io branch September 11, 2026 19:20
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.

haskell: the #1312 point-free guard drops any arrowless top-level signature — including the canonical entry :: IO ()

1 participant