Skip to content

fix(typeselector): accept the arguments Unity can actually serialize - #175

Merged
VPDPersonal merged 2 commits into
fix/generic-candidate-argument-compatibilityfrom
fix/unity-builtin-generic-arguments
Aug 9, 2026
Merged

fix(typeselector): accept the arguments Unity can actually serialize#175
VPDPersonal merged 2 commits into
fix/generic-candidate-argument-compatibilityfrom
fix/unity-builtin-generic-arguments

Conversation

@VPDPersonal

@VPDPersonal VPDPersonal commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

The argument filter for open generics asked Type.IsSerializable. That is the wrong question twice over, and each half cost the picker rows it should have offered. Two commits, each meaningful on its own.

1. Type.IsSerializable is false for everything Unity serializes natively — the engine writes the layout of Vector2, Color, Quaternion, AnimationCurve and the rest itself instead of going through .NET serialization, so none of them carries [Serializable]. float only ever passed by short-circuiting on IsPrimitive.

Before After
IConverter<Vector2, Vector2> field SequenceConverters<T> listed open SequenceConverters<Vector2>, closed
Its argument page would not offer Vector2 either offers it
  • 🎯 The engine's set is now named explicitly, next to the IsSerializable branch and with a comment on why it is not a duplicate of it: Vector2/3/4, Vector2Int/3Int, Quaternion, Matrix4x4, Color, Color32, Gradient, Rect, RectInt, Bounds, BoundsInt, LayerMask, AnimationCurve, PropertyName, SphericalHarmonicsL2.
  • 📏 Membership was measured, not assumed. Every candidate got a real field on a ScriptableObject and was looked up through SerializedObject on 6000.4. That is what keeps Ray, Ray2D, Plane, RangeInt, Keyframe and GradientColorKey out — value types of the same family the engine does not serialize — and why Hash128, Pose, BoneWeight, RectOffset, GUIStyle and Scene are not repeated: they carry the attribute and already passed.

2. Even for a type Unity cannot serialize, the obligation often does not exist. SequenceConverters<T> holds nothing but a [SerializeReference] IConverter<T, T>[] — references, never T's layout — so every T closes it safely, yet an IConverter<Ray, Ray> field could not select it, and neither could a field over any plain class without [Serializable].

  • 🎯 GenericArgumentRequirement answers the question per type parameter: does it reach a field Unity writes by value? The walk follows arrays and List<T>, descends into nested [Serializable] types, counts inherited fields, and steps over static, const, readonly, [NonSerialized] and — the case it exists for — anything behind [SerializeReference]. Each of those rules was measured the same way as the type set above.
  • 🛡️ It establishes only the absence of the obligation. A shape it cannot follow, or a nesting deeper than it descends, keeps the strict rule. The two failure directions are not symmetrical: a missed rule can then only leave today's behaviour in place, never admit an argument whose data Unity silently drops. That asymmetry is what makes an incomplete model of Unity's rules safe to ship.
  • 🔌 Threaded as GenericArgumentFilter (definition, parameter, argument), because the answer is a property of the position, not of the type. It reaches the resolver through the new TypeSelectorFilter.InferredArgumentFilter.
  • ✂️ Deliberately separate from ArgumentFilter. That one curates the argument page — a list a human reads, which has to stay finite — so it goes on offering serializable types only. This one judges a single argument the field has already fixed, where nobody is browsing and the exact question can be afforded. Keeping them apart is also what stops an unconstrained parameter's page from expanding to the whole domain.

Verification

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

Field Before After
IConverter<Vector2, Vector2> SequenceConverters<open> SequenceConverters<Vector2>
IConverter<Color, Color> SequenceConverters<open> SequenceConverters<Color>
IConverter<Ray, Ray> both candidates open, unpickable SequenceConverters<Ray>, GenericFuncConverter<Ray, Ray>
IConverter<StringBuilder, StringBuilder> both open both closed
IConverter<float, float> / <string, string> unchanged

The walk was also run over every generic definition in the Aspid assemblies: no obligation for SequenceConverters<T> (its only field is the [SerializeReference] array), GenericFuncConverter<,> (a Func<,> field, not serialized) and GenericToString<T> (a string field that never mentions T), and an obligation kept for 93 of the parameters across 176 definitions.

Check Result
EditMode suite ✅ 392/392
Commit 1 cases fail against the previous behaviour ✅ 17/17
Commit 2 cases fail against the previous behaviour ✅ 5/5 negative cases

Twenty-six cases are added for the built-in set (accepted types, the Ray/Plane/RangeInt boundary, structurally ineligible arguments) and fourteen for the requirement walk (by-value field, public field, array, List<T>, nested [Serializable], inherited field, [SerializeReference]-only, never-serialized modifiers, a non-[Serializable] wrapper, a self-referencing type, and the unwalkable-definition fallback), plus two end-to-end cases through GetAssignableGenericDefinitions.

Notes for review

  • 📐 TypeSelectorFilter gains a property rather than changing one. ArgumentFilter keeps its type and meaning; InferredArgumentFilter is new and optional, so no existing caller of TypeSelectorWindow.Show breaks. The internal GenericTypeResolver signatures did change, along with the three tests that passed a bare Func<Type, bool>.
  • ⚠️ Not exercised through the picker UI. Verification went through direct calls on a live Editor, not by clicking the dropdown. Two manual checks are added to the QA checklist.
  • 🔎 A mirror defect is left alone, out of scope here. The measurement also showed IsSerializable accepting DateTime, TimeSpan, Guid, Uri, decimal and IntPtr — none of which Unity writes to a field. Tightening that removes rows rather than adding them, so it deserves its own change.
  • 📋 Docs synced per repo rules: CHANGELOG EN/RU, QA-CHECKLIST EN/RU, SerializeReferences.md EN/RU, Types.md EN/RU.
  • 🌿 Branched off fix/generic-candidate-argument-compatibility (fix(typeselector): drop a generic candidate the field cannot close #174), not main — that PR is still open and touches the neighbouring code. Rebase onto main once fix(typeselector): drop a generic candidate the field cannot close #174 lands.
🇷🇺 Описание на русском

Суть

Фильтр аргументов для открытых generic-типов спрашивал Type.IsSerializable. Это неверный вопрос сразу по двум причинам, и каждая половина стоила пикеру строк, которые он был обязан предложить. Два коммита, каждый осмыслен сам по себе.

1. Type.IsSerializable равен false для всего, что Unity сериализует нативно — движок сам пишет раскладку Vector2, Color, Quaternion, AnimationCurve и остальных, минуя сериализацию .NET, поэтому ни один из них не несёт [Serializable]. float проходил лишь потому, что ветка коротко замыкалась на IsPrimitive.

Было Стало
Поле IConverter<Vector2, Vector2> SequenceConverters<T> в списке открытым SequenceConverters<Vector2>, закрытым
Его страница аргументов Vector2 тоже не предлагала предлагает
  • 🎯 Набор движка теперь перечислен явно, рядом с веткой IsSerializable и с комментарием, почему это не её дубликат: Vector2/3/4, Vector2Int/3Int, Quaternion, Matrix4x4, Color, Color32, Gradient, Rect, RectInt, Bounds, BoundsInt, LayerMask, AnimationCurve, PropertyName, SphericalHarmonicsL2.
  • 📏 Состав набора измерен, а не предположен. Каждый кандидат получил настоящее поле на ScriptableObject и был найден через SerializedObject на 6000.4. Именно это оставило за бортом Ray, Ray2D, Plane, RangeInt, Keyframe и GradientColorKey — значимые типы того же семейства, которые движок не сериализует, — и поэтому же не продублированы Hash128, Pose, BoneWeight, RectOffset, GUIStyle и Scene: они несут атрибут и проходили и раньше.

2. Даже для типа, который Unity сериализовать не умеет, обязательства часто просто нет. SequenceConverters<T> не хранит ничего, кроме [SerializeReference] IConverter<T, T>[] — ссылки, никогда не раскладку T, — так что любой T закрывает его безопасно; тем не менее поле IConverter<Ray, Ray> выбрать его не могло, как и поле над любым обычным классом без [Serializable].

  • 🎯 GenericArgumentRequirement отвечает на вопрос по каждому параметру типа: доходит ли он до поля, которое Unity пишет по значению? Обход идёт по массивам и List<T>, спускается во вложенные [Serializable]-типы, учитывает унаследованные поля и перешагивает static, const, readonly, [NonSerialized] и — ради чего он и существует — всё, что стоит за [SerializeReference]. Каждое из этих правил измерено так же, как и набор типов выше.
  • 🛡️ Он устанавливает только отсутствие обязательства. Форма, которую он пройти не смог, или вложенность глубже, чем он спускается, сохраняют строгое правило. Два направления ошибки несимметричны: пропущенное правило может лишь оставить сегодняшнее поведение, но никогда не пропустит аргумент, чьи данные Unity молча потеряет. Именно эта асимметрия делает неполную модель правил Unity безопасной для поставки.
  • 🔌 Проброшен как GenericArgumentFilter (определение, параметр, аргумент), потому что ответ — свойство позиции, а не типа. До резолвера он доходит через новое TypeSelectorFilter.InferredArgumentFilter.
  • ✂️ Намеренно отделён от ArgumentFilter. Тот курирует страницу аргументов — список, который читает человек и который обязан оставаться конечным, — поэтому он по-прежнему предлагает только сериализуемые типы. Этот же судит один аргумент, уже зафиксированный полем, где никто не просматривает список и точный вопрос можно себе позволить. Их разделение — ещё и то, что не даёт странице неограниченного параметра разрастись до всего домена.

Проверка

Живые редакторы 6000.4.0f1, на настоящей иерархии IConverter из Aspid.MVVM:

Поле Было Стало
IConverter<Vector2, Vector2> SequenceConverters<open> SequenceConverters<Vector2>
IConverter<Color, Color> SequenceConverters<open> SequenceConverters<Color>
IConverter<Ray, Ray> оба кандидата открыты, выбрать нельзя SequenceConverters<Ray>, GenericFuncConverter<Ray, Ray>
IConverter<StringBuilder, StringBuilder> оба открыты оба закрыты
IConverter<float, float> / <string, string> без изменений

Обход прогнан и по всем generic-определениям в сборках Aspid: обязательства нет у SequenceConverters<T> (его единственное поле — массив под [SerializeReference]), GenericFuncConverter<,> (поле Func<,>, не сериализуется) и GenericToString<T> (поле string, вообще не упоминающее T); обязательство сохранено для 93 параметров из 176 определений.

Проверка Результат
Набор EditMode ✅ 392/392
Кейсы коммита 1 падают на прежнем поведении ✅ 17/17
Кейсы коммита 2 падают на прежнем поведении ✅ 5/5 негативных кейсов

Добавлено двадцать шесть кейсов на встроенный набор (принимаемые типы, граница Ray/Plane/RangeInt, структурно непригодные аргументы) и четырнадцать — на обход обязательства (поле по значению, публичное поле, массив, List<T>, вложенный [Serializable], унаследованное поле, только [SerializeReference], никогда не сериализуемые модификаторы, обёртка без [Serializable], самоссылающийся тип и откат на непроходимом определении), плюс два сквозных кейса через GetAssignableGenericDefinitions.

Заметки для ревью

  • 📐 TypeSelectorFilter получает новое свойство, а не меняет существующее. ArgumentFilter сохраняет тип и смысл; InferredArgumentFilter — новое и необязательное, так что ни один существующий вызов TypeSelectorWindow.Show не ломается. Внутренние сигнатуры GenericTypeResolver изменились — вместе с тремя тестами, передававшими голый Func<Type, bool>.
  • ⚠️ Через UI пикера не прогонялось. Проверка шла прямыми вызовами на живом редакторе, а не кликами по выпадающему списку. В QA-чек-лист добавлены две ручные проверки.
  • 🔎 Зеркальный дефект оставлен как есть, вне области этого PR. Измерение показало и обратное: IsSerializable принимает DateTime, TimeSpan, Guid, Uri, decimal и IntPtr — ни один из которых Unity в поле не пишет. Ужесточение здесь строки убирает, а не добавляет, поэтому заслуживает отдельного изменения.
  • 📋 Документация синхронизирована по правилам репозитория: CHANGELOG EN/RU, QA-CHECKLIST EN/RU, SerializeReferences.md EN/RU, Types.md EN/RU.
  • 🌿 Ответвление от fix/generic-candidate-argument-compatibility (fix(typeselector): drop a generic candidate the field cannot close #174), а не от main — тот PR ещё открыт и трогает соседний код. Перебазировать на main после того, как fix(typeselector): drop a generic candidate the field cannot close #174 будет влит.

🤖 Generated with Claude Code

VPDPersonal and others added 2 commits August 9, 2026 01:52
The argument filter ended in `Type.IsSerializable`, which is false for every
type the engine serializes natively: Unity writes their layout itself instead of
going through .NET serialization, so `Vector2`, `Color`, `Quaternion`,
`AnimationCurve` and the rest carry no `[Serializable]`. `float` only ever
passed by short-circuiting on `IsPrimitive`.

The visible cost was a row that could not be completed. A `SequenceConverters<T>
: IConverter<T, T>` candidate on an `IConverter<Vector2, Vector2>` field is fully
determined by the field, but inference re-applies the argument filter, so the
row fell back to its open definition — and the argument page, filtered by the
same predicate, would not offer `Vector2` either.

The set is now named explicitly next to the `IsSerializable` branch, with a
comment on why it is not a duplicate of it. Membership was measured rather than
assumed: every candidate was given a real field on a ScriptableObject and looked
up through SerializedObject on 6000.4. That is what keeps `Ray`, `Ray2D`,
`Plane`, `RangeInt`, `Keyframe` and `GradientColorKey` out — value types of the
same family the engine does not serialize — and it is why `Hash128`, `Pose`,
`BoneWeight`, `RectOffset`, `GUIStyle` and `Scene` are not repeated: they carry
the attribute and already pass.

The measurement also turned up the mirror defect, left alone here as it is not
this fix: `IsSerializable` accepts `DateTime`, `TimeSpan`, `Guid`, `Uri`,
`decimal` and `IntPtr`, none of which Unity writes to a field.

Verified on the live 6000.4.0f1 Editors. EditMode suite green (378/378); the 17
new cases each fail against the previous behaviour. Against MVVM's real
`IConverter`, `<Vector2,Vector2>`, `<Color,Color>` and `<Quaternion,Quaternion>`
now list `SequenceConverters` closed over the argument, with `<float,float>` and
`<string,string>` unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tored

The argument filter asked whether a type is Unity-serializable. What decides
anything is whether the closed type would *store* it. `SequenceConverters<T>`
holds nothing but a `[SerializeReference] IConverter<T, T>[]` — the engine
writes references there, never T's layout — so every T closes it safely, yet an
`IConverter<Ray, Ray>` field could not select the candidate at all, and neither
could one over any plain class without `[Serializable]`.

GenericArgumentRequirement answers the right question per type parameter: does
it reach a field Unity writes by value? The walk follows arrays and List<T>,
descends into nested `[Serializable]` types, counts inherited fields, and steps
over `static`, `const`, `readonly`, `[NonSerialized]` and — the case it exists
for — anything behind `[SerializeReference]`. Every one of those rules was
measured against a real field on a ScriptableObject rather than recalled.

It is written to establish only the *absence* of the obligation: a shape it
cannot follow, or a nesting deeper than it descends, keeps the strict rule. The
two failure directions are not symmetrical — a missed rule can then only leave
today's behaviour in place, never admit an argument whose data Unity silently
drops — and that asymmetry is what makes an incomplete model of Unity's rules
safe to ship.

The filter is threaded as GenericArgumentFilter, which takes the definition and
the parameter alongside the argument, because the answer is a property of the
position rather than of the type. It reaches the resolver through the new
TypeSelectorFilter.InferredArgumentFilter, deliberately separate from
ArgumentFilter: that one curates the argument page, which is a list a human
reads and must stay finite, so it goes on offering serializable types only. This
one judges a single argument the field has already fixed, where nobody is
browsing and the exact question can be afforded.

Verified on the live 6000.4.0f1 Editors. EditMode suite green (392/392); the
five new negative cases each fail against the previous behaviour. Against MVVM's
real converters the walk reports no obligation for `SequenceConverters<T>`
(its only field is the [SerializeReference] array), `GenericFuncConverter<,>`
and `GenericToString<T>`, and keeps one for 93 of the 176 generic definitions in
the domain. `IConverter<Ray, Ray>` and `IConverter<StringBuilder, StringBuilder>`
fields now list their candidates closed, with `<Vector2,Vector2>`,
`<Color,Color>`, `<float,float>` and `<string,string>` unchanged.

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/unity-builtin-generic-arguments 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