Skip to content

fix(typeselector): drop a generic candidate the field cannot close - #174

Merged
VPDPersonal merged 1 commit into
mainfrom
fix/generic-candidate-argument-compatibility
Aug 9, 2026
Merged

fix(typeselector): drop a generic candidate the field cannot close#174
VPDPersonal merged 1 commit into
mainfrom
fix/generic-candidate-argument-compatibility

Conversation

@VPDPersonal

Copy link
Copy Markdown
Owner

Summary

The candidate scan matched the two generic definitions and ignored their arguments, so a candidate that fixes an argument itself was offered for a field it can never fit:

Before After
IConverter<float, float> field GenericToString<TFrom> listed dropped
After picking it argument page refuses every choice

GenericToString<TFrom> : IConverter<TFrom, string> is an IConverter<,>, yet no TFrom makes the closed type assignable. Inference then failed (correctly), the caller fell back to the open definition, and the row became a dead end. The whole numeric converter family in Aspid.MVVM showed it.

  • 🎯 CanCloseToFieldType now compares the arguments too, position by position, rather than matching definitions alone. GenericBaseDefinitions became dead and is removed.
  • ↔️ The comparison honours declared variance, because assignability does. With IConverter<in TFrom, out TTo> an IConverter<object, string> candidate is still listed for an IConverter<string, string> field. A purely structural comparison would have traded this defect for its mirror image — a usable candidate missing from the list, which is not hypothetical: an IConverter<float, object> field legitimately accepts GenericToString<float>.
  • It also honours where variance stops. The CLR applies variance only across an implicit reference conversion, so a position the field closed over a value type admits exactly one argument, like an invariant one. Note that IsAssignableFrom alone does not express this — typeof(object).IsAssignableFrom(typeof(int)) is true by boxing — hence an explicit guard.
  • 🔁 Pinned positions are matched first and their bindings recorded; variant positions are judged afterwards, against what the pinned ones already forced. That ordering is what also drops SequenceConverters<T> : IConverter<T, T> from an IConverter<float, string> field — the same dead row, found while verifying this fix — without depending on which position happens to be declared first.
  • ↩️ A position that still admits a family of arguments is left alone. Pair<TKey, TValue> under an IKeyed<string> field keeps its open definition and still opens the argument page. Proving no argument converts would mean sweeping the domain — work the page already does, validating each choice through TryConstruct.

Verification

Live 6000.4.0f1 Editor, against Aspid.MVVM's real IConverter hierarchy:

Field Candidates after the fix
IConverter<float, float> GenericFuncConverter<Single, Single>, SequenceConverters<Single>GenericToString gone (the reported defect)
IConverter<float, string> GenericFuncConverter<Single, String>, GenericToString<Single>SequenceConverters<open> gone (side finding)
IConverter<float, object> GenericFuncConverter<Single, Object>, GenericToString<open> — variance kept the usable row
IConverter<string, string> unchanged, all three closed
Check Result
EditMode suite ✅ 352/352
New cases fail against the previous behaviour ✅ 4/4 negative cases
Brute-force cross-check over 7 field shapes (every candidate closed over an argument pool, tested with IsAssignableFrom) ✅ no over-rejection

Nine cases are added to GenericTypeResolverTests, covering the repro, the inverse (candidate fixes an argument the field agrees with), the determined and partially-determined candidates, variance on a reference type, two value-type rejections, and a definition implemented twice.

Notes for review

  • run_tests can be driven from this CLI, contrary to the note in fix(typeselector): list a determined generic candidate closed #173: run_tests --mode EditMode --async_tests true plus polling test_status works. Only the synchronous mode hits the Pipeline server's 30 s cut-off.
  • ⚠️ Not exercised through the picker UI. Verification went through direct resolver calls on a live Editor, not by clicking the dropdown. A manual check is added to the QA checklist.
  • 🔎 One residual class of dead rows is left deliberately. When a single unconstrained parameter sits on two variant positions (SequenceConverters<T> under an IConverter<object, string> field), the pre-filter still passes it — the brute-force sweep reports offered=3 against closable=2. Ruling it out means enumerating the domain, which is the argument page's job; the behaviour predates this change and is documented in the <remarks>.
  • 📋 Docs synced per repo rules: CHANGELOG EN/RU, QA-CHECKLIST EN/RU, SerializeReferences.md EN/RU.

🤖 Generated with Claude Code

The candidate scan matched the two generic *definitions* and ignored their
arguments, so `GenericToString<TFrom> : IConverter<TFrom, string>` was offered
for an `IConverter<float, float>` field — the definitions agree, yet no TFrom
makes the closed type fit. Inference then failed (correctly), the caller fell
back to the open definition, and the row became a dead end: selecting it opened
the argument page, which refused every choice made on it. The whole numeric
converter family in Aspid.MVVM shows the row.

CanCloseToFieldType now compares the arguments too, position by position, and
honours declared variance while doing it — assignability does, so with
`IConverter<in TFrom, out TTo>` an `IConverter<object, string>` candidate is
still listed for an `IConverter<string, string>` field. A purely structural
comparison would have traded this defect for its mirror image, a usable
candidate missing from the list.

Where variance stops is part of the rule: the CLR only applies it across an
implicit reference conversion, so a position the field closed over a value type
admits exactly one argument, just like an invariant one. Those pinned positions
are matched first and their bindings recorded; the variant positions are judged
afterwards, against what the pinned ones already forced. That ordering is what
also removes `SequenceConverters<T> : IConverter<T, T>` from an
`IConverter<float, string>` field — the same dead row, found while verifying
this fix — without depending on which position happens to be declared first.

A position that still admits a family of arguments is left alone: proving that
none of them converts would mean sweeping the domain, which is what the argument
page does anyway, validating each choice through TryConstruct.

Verified on the live 6000.4.0f1 Editors. The FastTools EditMode suite is green
(352/352, seven new cases; the four negative ones each fail against the previous
behaviour). Against MVVM's real `IConverter`, the candidate lists now read:
`<float,float>` → GenericFuncConverter, SequenceConverters<Single>;
`<float,string>` → GenericFuncConverter, GenericToString<Single>;
`<float,object>` → GenericFuncConverter, GenericToString (open, variance);
`<string,string>` unchanged. A brute-force cross-check over seven field shapes —
every candidate closed over a pool of arguments and tested with IsAssignableFrom
— reports no over-rejection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VPDPersonal VPDPersonal added type: fix Bug fix area: editor Editor-only code area: docs Repository documentation (README, CHANGELOG, docs/) labels Aug 8, 2026
@VPDPersonal
VPDPersonal merged commit 173eab1 into main Aug 9, 2026
3 checks passed
@VPDPersonal
VPDPersonal deleted the fix/generic-candidate-argument-compatibility branch August 9, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Repository documentation (README, CHANGELOG, docs/) area: editor Editor-only code type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant