Skip to content

types: settle a shared field name once the binding is inferred - #54

Merged
simontreanor merged 1 commit into
mainfrom
fix/deferred-field-resolution
Jul 31, 2026
Merged

types: settle a shared field name once the binding is inferred#54
simontreanor merged 1 commit into
mainfrom
fix/deferred-field-resolution

Conversation

@simontreanor

Copy link
Copy Markdown
Owner

From the other repo's feedback that #37 did not fully remove the field-name problem. Testing it splits the report in two, and one half is fixable.

Map.tryFind is not the culprit. When the map's value type is pinned, the payload already resolves:

let board = Map.ofList [(1, Placed { … })]
match Map.tryFind k board:
  case Some p: p.letter        # ok today

It fails only when the map itself arrives unpinned, which is the same root cause as a bare lambda parameter — nothing determines the type — rather than anything about tryFind or Option payloads. There's a test for this so it stays true.

The fixable shape is a base that a later statement pins down:

let describe c =
    let l = c.letter        # rejected before: `c` unknown *here*
    let flag = c.blank      # …but `blank` determines `c`
    l

HM knows the answer by the end of the binding; #37 just asked too early. Such an access is now recorded and settled once the enclosing top-level binding is fully inferred.

The part worth review is generalization. A variable a pending obligation depends on stays monomorphic until settled. Block-level lets do generalize, so without that guard the deferred variable would be generalized, each use would take its own copy, and the later resolution would reach none of them — it would type-check and mean nothing. That failure mode is silent, which is why the guard is in generalize rather than relying on ordering.

Deferring never weakens the check. Three tests cover the ways it must still fail: the field's type has to fit every use (l + 1 where l is the string field is still an error), the field has to exist on the record that wins, and a value nothing pins anywhere is still an error.

The message for that last case now names both ways out, since it offered only the pattern form — the parameter form did not exist when it was written, and per the report it reads better:

field `letter` is ambiguous here: nothing says what this value is, and `letter` is
declared by records `Placed` and `Cell`. Name the record where the value arrives —
in a parameter (`fun (Placed { letter }) -> …`) or in a pattern
(`case Placed { letter }:`) — or give it a type some other way

Tests: 5 typecheck cases and an end-to-end one confirming the deferred access lowers to the ordinary attribute read. Full suite, clippy, fmt and the 23 doc lessons clean.

Feedback from the other repo: #37 resolves a field when the base's type is
known *at the access*, which left two shapes still ambiguous. Testing them
splits the report in two.

`Map.tryFind` is not one of them: when the map's value type is pinned, the
payload resolves already. It fails only when the map itself arrives
unpinned, which is the same root cause as a bare lambda parameter.

The fixable shape is a base that a *later* statement pins down. `let l =
c.letter` followed by `let flag = c.blank` was rejected even though `blank`
determines `c`, because resolution happened at first sight rather than when
HM had the answer. Such an access is now recorded and settled once the
enclosing top-level binding is fully inferred.

The part that makes this sound is generalization: a variable a pending
obligation depends on stays monomorphic until it is settled. Block-level
`let`s generalize, so without that guard the deferred variable would be
generalized, each use would take its own copy, and the later resolution
would reach none of them — it would type-check and mean nothing. Tests
cover the three ways it must still fail: the field's type has to fit its
uses, the field has to exist on the record that wins, and a value nothing
pins anywhere is still an error.

That last message now names both ways out. It offered only the pattern form
(`case Cell { letter }:`), because the parameter form
(`fun (Cell { letter }) -> …`) did not exist when it was written — and that
is the one the report found reads better.
@simontreanor
simontreanor merged commit c53d919 into main Jul 31, 2026
11 checks passed
@simontreanor
simontreanor deleted the fix/deferred-field-resolution branch July 31, 2026 20:07
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