Skip to content

checking: use expected result types in operator chains and lambdas#126

Open
bneiswander wants to merge 9 commits into
purefunctor:mainfrom
bneiswander:pr/checking-operator-expected-results
Open

checking: use expected result types in operator chains and lambdas#126
bneiswander wants to merge 9 commits into
purefunctor:mainfrom
bneiswander:pr/checking-operator-expected-results

Conversation

@bneiswander

Copy link
Copy Markdown
Contributor

checking: use expected result types in operator chains and lambdas

Base branch: pr/checking-row-fundep-givens

Review Order

This PR is stacked on pr/checking-row-fundep-givens and should be reviewed after that PR.

Base branch for review: pr/checking-row-fundep-givens

It is otherwise independent from pr/checking-instance-chain-backtracking; both branches can be reviewed after the row-fundep/givens PR is understood.

Summary

  • Propagate simple expected result types into operator-chain checking.
  • Check lambda bodies under skolemized expected signatures so contextual function types refine lambda bodies correctly.
  • Defer expected-result subtyping when operator results are still unresolved, avoiding premature false-positive diagnostics.

Motivation

The analyzer reported false positives in code accepted by PureScript when expected types came from surrounding operator chains or lambda contexts. The underlying issue was that expected result information was either not propagated far enough, or was applied too early before operator result constraints had resolved.

Expected result information should improve checking when it is simple and known, but should not force a subtyping decision while the operator result is still constrained by unresolved instance solving.

Motivating Example

The checker should use the expected result type from the surrounding signature when checking an operator chain:

program :: Effect Unit
program = do
  log "start" *> log "done"

It should also avoid rejecting constrained operator results before instance solving has determined the final result type.

Implementation Notes

  • Extends operator-chain checking to pass through simple expected result types where doing so improves inference without changing unconstrained inference behavior.
  • Adds lambda expected-signature handling that skolemizes the expected function type before checking the body.
  • Defers expected-result subtyping for unresolved operator results so constraint solving can determine the result first.
  • This branch is stacked on pr/checking-row-fundep-givens because lambda expected-signature checking uses the given-elaboration support introduced there.

Tests

Added fixtures:

  • 1778726580_operator_chain_expected_result
  • 1778728380_operator_chain_inference_unchanged
  • 1778782440_operator_chain_nested_hoist_context
  • 1778784780_operator_constrained_result_monad_effect

Validation run locally:

cargo check -p checking --tests

Fixes CannotUnify diagnostics when using polykinded row type classes
like SafeUnion (forall k. Row k -> Row k -> Row k -> Constraint) with
higher-kinded effect rows (Row (Type -> Type)).

Two separate issues were addressed:

1. Functional-dependency positions in canonical constraint elaboration
   (elaborate_given_substitution) were indexing into canonical argument
   lists without accounting for implicit kind binders. The canonical
   representation interleaves kind arguments (from forall k.) before
   type arguments, so position 0 in a class's type parameters actually
   maps to canonical argument index 1 when a kind binder is present.
   Fixed in both elaborate.rs (speculative solving) and matching.rs
   (can_determine_stuck).

2. The Prim.Row.Union compiler solver created residual open-row tails
   (the | r part of (a :: A | r)) using a hardcoded Row Type kind.
   For polykinded classes, this caused the kind to be stuck as Row Type
   even when the constraint involved Row (Type -> Type). Fixed by
   inferring the correct row element kind from the other arguments and
   using that to construct the fresh tail variable.

Also adds a regression fixture that reproduces the SafeUnion pattern
using Eval/RowList/FromRow/ToRow (mimicking Type.Eval.Row.Util.SafeUnion)
and verifies it type-checks correctly.
When functional-dependency elaboration produces a substitution for a
forall-bound rigid variable, the improved givens now extend the original
givens rather than replacing them. This allows visible type applications
against those variables in the function body (e.g. Proxy @x) to still
resolve against the explicit constraint while the improved forms are
available for argument and result substitution.

Regression test: eval_given_visible_type_application
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c62c8cc-a883-4b0e-b698-89e120141b51

📥 Commits

Reviewing files that changed from the base of the PR and between 0f7ea9b and 9ebae49.

⛔ Files ignored due to path filters (9)
  • tests-integration/fixtures/checking/1772440380_prim_row_apart/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1772440500_prim_row_generalization/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1772442660_prim_type_error_warn/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1772442720_prim_type_error_fail/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1777298340_top_level_fail_eager/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1778726580_operator_chain_expected_result/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1778728380_operator_chain_inference_unchanged/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1778782440_operator_chain_nested_hoist_context/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1778784780_operator_constrained_result_monad_effect/Main.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • compiler-core/checking/src/core/constraint/elaborate.rs
  • compiler-core/checking/src/source/operator.rs
  • compiler-core/checking/src/source/terms/forms.rs
  • tests-integration/fixtures/checking/1778726580_operator_chain_expected_result/Main.purs
  • tests-integration/fixtures/checking/1778728380_operator_chain_inference_unchanged/Main.purs
  • tests-integration/fixtures/checking/1778782440_operator_chain_nested_hoist_context/Main.purs
  • tests-integration/fixtures/checking/1778784780_operator_constrained_result_monad_effect/Main.purs
✅ Files skipped from review due to trivial changes (1)
  • tests-integration/fixtures/checking/1778784780_operator_constrained_result_monad_effect/Main.purs
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests-integration/fixtures/checking/1778728380_operator_chain_inference_unchanged/Main.purs
  • compiler-core/checking/src/source/operator.rs
  • compiler-core/checking/src/core/constraint/elaborate.rs
  • tests-integration/fixtures/checking/1778782440_operator_chain_nested_hoist_context/Main.purs
  • compiler-core/checking/src/source/terms/forms.rs

Walkthrough

Adds a Name->Type elaboration routine for given constraints, refactors functional-dependency lookup to use state-aware toolkit queries, applies elaboration during term/lambda checking, optimizes operator result checking and row-union freshening, and adds multiple PureScript integration fixtures exercising these flows.

Changes

Functional Dependency Elaboration and Constraint System

Layer / File(s) Summary
Functional Dependency Lookup Refactoring
compiler-core/checking/src/core/constraint/matching.rs
Removes TypeItemIr usage and threads &mut CheckState through can_determine_stuck/get_functional_dependencies, delegating functional-dependency lookup to toolkit::lookup_file_class.
Elaboration Core Implementation
compiler-core/checking/src/core/constraint/elaborate.rs, compiler-core/checking/src/core/constraint.rs
Adds elaborate_given_substitution: expands determined args with fresh vars, probe-solves temporary constraints, zonks results, accumulates Name->Type improvements, and exposes a public wrapper that canonicalises inputs.
Constraint Elaboration in Term Checking
compiler-core/checking/src/source/terms/equations.rs, compiler-core/checking/src/source/terms/forms.rs
Compute a given-substitution from produced constraints, and when non-empty apply it to constraints, argument types, and the expected result before continuing equation/lambda checking.
Operator Result Check & Row Constraint Fixes
compiler-core/checking/src/source/operator.rs, compiler-core/checking/src/core/constraint/compiler/prim_row.rs
Introduce an early eligibility-gated operator result subtype fast-path that checks result types before operand traversal when safe; derive row constraint kind when generating fresh row unification targets for open-left unions.
Integration Test Fixtures
tests-integration/fixtures/checking/*/Main.purs
Adds multiple PureScript fixtures exercising row functional dependencies, polykinded safe unions and composition, visible type application evaluation, and operator-chain inference/hoisting scenarios.

Sequence Diagram

sequenceDiagram
  participant TermChecker as TermChecker (equations/forms)
  participant Elaborate as elaborate_given_substitution
  participant Matching as Matching (get_functional_dependencies)
  participant Toolkit as toolkit::lookup_file_class
  participant Solve as solve_constraints

  TermChecker->>Elaborate: request elaboration for given constraints
  Elaborate->>Matching: ask for functional dependencies
  Matching->>Toolkit: lookup_file_class(state, context, type_id)
  Toolkit-->>Matching: class info with functional_dependencies
  Matching-->>Elaborate: functional dependency data
  Elaborate->>Elaborate: expand determined args, create fresh vars
  Elaborate->>Solve: probe-solve temporary constraint set
  Solve-->>Elaborate: derived type improvements
  Elaborate-->>TermChecker: NameToType substitution
  TermChecker->>TermChecker: apply substitution to constraints/args/result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A rabbit hops through constraint night,

Names become types in morning light,
Fresh vars sprout, conflicts kept small,
Substitutions tumble, then stand tall,
Hooray for checks that catch them all!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset by explaining the motivation, implementation notes, and testing approach for propagating expected result types in operator chains and lambdas.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

…in checking

Move the expected-result subtype check before operand traversal in
traverse_operator_branch so that unification variables in simple operator
signatures are solved before operands are checked. This fixes false-
positive CannotUnify diagnostics for flipped application (#) pipelines
over polymorphic natural transformations.

Higher-rank, constrained, and function-shaped expected results are
checked after operands to avoid premature skolemisation.

Adds regression fixture 1778726580_operator_chain_expected_result.
Regression for spec requirement 'Operator Chain Inference Remains
Unchanged': verify that operator chains inferred without an expected
type still produce correct inferred types.
…r results

When an operator-chain expression applies a constrained polymorphic
function (e.g. MonadEffect m => m a) the early expected-result
subtyping can unify the constrained result directly with the expected
type in a non-elaborating context, emitting a false CannotUnify.

Defer the early result check when the operator result is still an
unresolved unification variable, letting operand checking contribute
type information so wanted constraints can be solved first.
@bneiswander bneiswander force-pushed the pr/checking-operator-expected-results branch from 0f7ea9b to 9ebae49 Compare May 15, 2026 16:25
@bneiswander bneiswander changed the title Pr/checking operator expected results checking: use expected result types in operator chains and lambdas May 15, 2026
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