Consolidation plan C3 / M5. When behaviour moves from struct instance methods to
<Type>Intrinsicsextension classes, THIS is the surface that must stay ON the struct (or otherwise remain callable in idiomatic C#). Enforced byIntrinsicsApiSnapshotTests— any change to the public surface of a listed type is a reviewable test diff, so a loss (a member vanishing from both the struct and its extension class) can never slip through unnoticed.
Scalar-erased (already ported to <Type>Intrinsics extensions): Number, Integer, Boolean.
No behaviour to move: Character, String (conversions + IArray obligations only).
Not yet ported (M5 target): Angle, Vector2, Vector3, Vector4, Vector8,
Matrix3x2, Matrix4x4, Quaternion, Plane.
- Fields — the backing data (
Value,X/Y/Z,M11…). Not movable; they are the state. - Constructors —
new Vector3(x, y, z),new Vector3(SNVector3), broadcastnew Vector3(x). C# constructors cannot be extensions. - Operators (
op_Addition,op_Multiply,op_Equality,op_UnaryNegation, …) — C# does not permit extension operators on our target LangVersion, anda + bis core ergonomics. Keep. - Implicit / explicit conversions (
op_Implicit,op_Explicit) — same reason; also load-bearing for the scalar-erasure boundary (Number⇄float). Keep. - Field-style properties consumers rely on: the component accessors (
X,Y,Z,M11…),Count, and the cheap pseudo-fields. These read as data in C# (v.X) and moving them to methods would break every consumer and the cost-model's field-projection recognition. Keep as properties. - Static factory / constant helpers:
Zero,One,UnitX/Y/Z/W,Default,Identity,MinValue,MaxValue,Create*. IdiomaticVector3.UnitXmust keep working. Keep as statics on the struct. - BCL / framework obligations:
IReadOnlyList<T>members,IEquatable<T>.Equals,GetHashCode,ToString,Deconstruct,DataContract/DataMember. Interface obligations cannot be extensions. Keep.
Everything else — the behavioural instance methods: Cos, Sin, Normalize, Dot, Cross,
Transform, Length, Lerp, the swizzles-as-methods, etc. These become
public static R Foo(this T self, …) in <Type>Intrinsics, exactly as Number/Integer/Boolean
already did. Consumer call syntax (v.Normalize(), a.Cos()) is unchanged because extension methods
are invoked with the same receiver.Method() syntax.
Passing an instance method as a method group where a delegate is expected
(someList.Select(v.Normalize)) does not work for extension methods — but v => v.Normalize()
does, and the generated Plato surface never does this. No known consumer relies on it.
The emitter currently generates, per handwritten intrinsic function, a forwarder extension
TExtensions.Foo(this T self) => self.Foo() that also performs the Number→float return erasure.
Once Foo is itself an extension, self.Foo() is CS0121-ambiguous and self-recursive. The emitter
must instead emit the forwarder as an explicit static call to the intrinsic —
TExtensions.Foo(this T self) => TIntrinsics.Foo(self) — preserving the return erasure. That is the
emitter half of M5; the snapshot test guards the runtime half.