fix(typeselector): accept the arguments Unity can actually serialize - #175
Merged
VPDPersonal merged 2 commits intoAug 9, 2026
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.IsSerializableisfalsefor everything Unity serializes natively — the engine writes the layout ofVector2,Color,Quaternion,AnimationCurveand the rest itself instead of going through .NET serialization, so none of them carries[Serializable].floatonly ever passed by short-circuiting onIsPrimitive.IConverter<Vector2, Vector2>fieldSequenceConverters<T>listed openSequenceConverters<Vector2>, closedVector2eitherIsSerializablebranch 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.ScriptableObjectand was looked up throughSerializedObjecton 6000.4. That is what keepsRay,Ray2D,Plane,RangeInt,KeyframeandGradientColorKeyout — value types of the same family the engine does not serialize — and whyHash128,Pose,BoneWeight,RectOffset,GUIStyleandSceneare 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, neverT's layout — so everyTcloses it safely, yet anIConverter<Ray, Ray>field could not select it, and neither could a field over any plain class without[Serializable].GenericArgumentRequirementanswers the question per type parameter: does it reach a field Unity writes by value? The walk follows arrays andList<T>, descends into nested[Serializable]types, counts inherited fields, and steps overstatic,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.GenericArgumentFilter(definition, parameter, argument), because the answer is a property of the position, not of the type. It reaches the resolver through the newTypeSelectorFilter.InferredArgumentFilter.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.0f1Editors, against Aspid.MVVM's realIConverterhierarchy: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>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<,>(aFunc<,>field, not serialized) andGenericToString<T>(astringfield that never mentionsT), and an obligation kept for 93 of the parameters across 176 definitions.Twenty-six cases are added for the built-in set (accepted types, the
Ray/Plane/RangeIntboundary, 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 throughGetAssignableGenericDefinitions.Notes for review
TypeSelectorFiltergains a property rather than changing one.ArgumentFilterkeeps its type and meaning;InferredArgumentFilteris new and optional, so no existing caller ofTypeSelectorWindow.Showbreaks. The internalGenericTypeResolversignatures did change, along with the three tests that passed a bareFunc<Type, bool>.IsSerializableacceptingDateTime,TimeSpan,Guid,Uri,decimalandIntPtr— none of which Unity writes to a field. Tightening that removes rows rather than adding them, so it deserves its own change.SerializeReferences.mdEN/RU,Types.mdEN/RU.fix/generic-candidate-argument-compatibility(fix(typeselector): drop a generic candidate the field cannot close #174), notmain— that PR is still open and touches the neighbouring code. Rebase ontomainonce 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]. Каждое из этих правил измерено так же, как и набор типов выше.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 определений.Добавлено двадцать шесть кейсов на встроенный набор (принимаемые типы, граница
Ray/Plane/RangeInt, структурно непригодные аргументы) и четырнадцать — на обход обязательства (поле по значению, публичное поле, массив,List<T>, вложенный[Serializable], унаследованное поле, только[SerializeReference], никогда не сериализуемые модификаторы, обёртка без[Serializable], самоссылающийся тип и откат на непроходимом определении), плюс два сквозных кейса черезGetAssignableGenericDefinitions.Заметки для ревью
TypeSelectorFilterполучает новое свойство, а не меняет существующее.ArgumentFilterсохраняет тип и смысл;InferredArgumentFilter— новое и необязательное, так что ни один существующий вызовTypeSelectorWindow.Showне ломается. Внутренние сигнатурыGenericTypeResolverизменились — вместе с тремя тестами, передававшими голыйFunc<Type, bool>.IsSerializableпринимаетDateTime,TimeSpan,Guid,Uri,decimalиIntPtr— ни один из которых Unity в поле не пишет. Ужесточение здесь строки убирает, а не добавляет, поэтому заслуживает отдельного изменения.SerializeReferences.mdEN/RU,Types.mdEN/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