Skip to content

Commit ba00f45

Browse files
authored
fix(binder,emit): implicit []T to IEnumerable[T] at static-call slots (#829)
Closes #821. The slice-to-interface implicit conversion (#570) and the sequence-vs-slice receiver-call extension (#774) both classified slice arguments correctly when the target interface's ClrType was a fully closed live-runtime instantiation. At static-method / free-function argument slots, the target's ClrType could be the type-erased open- definition form because Binder.SubstituteType keeps the erased ClrType on ImportedTypeSymbol.GetConstructed when MakeGenericType cannot close across reflection contexts (open def in MetadataLoadContext vs live runtime element type). The real substituted G# types live on ImportedTypeSymbol.TypeArguments, but neither the binder's slice-to-interface classifier nor the emitter's reference-compatibility walk consulted them, so the call site was rejected with GS0155 (binder) or GS9998 (emitter) depending on the path. Three localized fixes: - Conversion.SliceImplementsInterface: extends the slice-to-interface arm to fall back to the target's symbolic TypeArguments and the open definition's FullName when the constructed CLR target is erased, matching slice-array interface arguments by leaf FullName so the invariance guarantee is preserved. - MethodBodyEmitter.IsReferenceCompatible: mirrors the binder fallback so the emit-time reference upcast recognises the slice argument as CLR-equivalent to the erased ImportedTypeSymbol target and emits a no-op IL conversion. - ReflectionMetadataEmitter.GetErasedObjectArgs: routes erased-object generic instantiation through emitCtx.CoreObjectType when the open definition lives outside the host runtime, fixing "This type 'System.Object' was not loaded by the MetadataLoadContext" failures when synthesizing the receiver TypeSpec for a call whose parameter is a constructed CLR interface. Restores the original IEnumerable[T] signatures on Sequences.Indexed / Sequences.Pairwise in samples/TupleSequenceIterators.gs (the workaround for #813 was the sequence[T] spelling). Tests: - Binder: 9 new tests covering issue repro plus IEnumerable[T] / IList[T] / ICollection[T] / IReadOnlyList[T] at static-method and free-function argument slots, plus negative variance guards. - Emit: 7 new IL-verified CompileAndRun tests for the same shapes. - Interpreter: 6 new closed-instantiation tests through GSharpRepl. Related: #774 (open-generic receiver iteration), #813 (tuple-element iterator state machines, source of the workaround being reverted), #706 (parent / per-PR series). Verified: - dotnet test GSharp.sln green (5,253 passed, 1 skip baseline regen). - ilverify clean (IlVerifier in every new emit test). - website docs build clean (npm run build).
1 parent f182b96 commit ba00f45

7 files changed

Lines changed: 939 additions & 6 deletions

File tree

samples/TupleSequenceIterators.gs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import System.Collections.Generic
1414
class Sequences {
1515
shared {
1616
// `(int32, T)` element type — the issue's `Indexed` spelling.
17-
func Indexed[T](source sequence[T]) sequence[(int32, T)] {
17+
func Indexed[T](source IEnumerable[T]) sequence[(int32, T)] {
1818
var index = 0
1919
for v in source {
2020
yield (index, v)
@@ -23,7 +23,7 @@ class Sequences {
2323
}
2424

2525
// `(T, T)` element type — the issue's `Pairwise` spelling.
26-
func Pairwise[T](source sequence[T]) sequence[(T, T)] {
26+
func Pairwise[T](source IEnumerable[T]) sequence[(T, T)] {
2727
var first = true
2828
var prev T = default(T)
2929
for v in source {

src/Core/CodeAnalysis/Binding/Conversion.cs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ public static Conversion Classify(TypeSymbol from, TypeSymbol to)
462462
if (from is SliceTypeSymbol sliceForIface && to?.ClrType != null && to.ClrType.IsInterface)
463463
{
464464
if (sliceForIface.ClrType != null
465-
&& ClrTypeUtilities.ImplementsInterfaceByName(sliceForIface.ClrType, to.ClrType))
465+
&& SliceImplementsInterface(sliceForIface, to))
466466
{
467467
return Conversion.Implicit;
468468
}
@@ -733,4 +733,86 @@ private static bool IsTupleClrEquivalent(TypeSymbol a, TypeSymbol b)
733733

734734
return false;
735735
}
736+
737+
/// <summary>
738+
/// Issue #821: cross-context-safe slice-to-interface check. When the
739+
/// target is a constructed <see cref="ImportedTypeSymbol"/> whose
740+
/// <c>ClrType</c> is the type-erased open-definition form (because
741+
/// <see cref="Type.MakeGenericType(Type[])"/> could not be closed at
742+
/// substitution time — typically because the open definition lives in a
743+
/// <see cref="System.Reflection.MetadataLoadContext"/> while the
744+
/// substituted argument types live in the live runtime) the symbolic
745+
/// <see cref="ImportedTypeSymbol.TypeArguments"/> carry the real
746+
/// substituted G# types. Match against those, not the erased CLR
747+
/// arguments, so the slice still classifies as implicitly convertible to
748+
/// the constructed interface.
749+
/// </summary>
750+
/// <param name="slice">The source slice type.</param>
751+
/// <param name="targetInterface">The constructed interface target.</param>
752+
/// <returns><see langword="true"/> when the slice's backing array
753+
/// implements the target interface.</returns>
754+
private static bool SliceImplementsInterface(SliceTypeSymbol slice, TypeSymbol targetInterface)
755+
{
756+
// Cheap path: the target's CLR generic arguments are already the
757+
// properly substituted closed form. Defer to the existing
758+
// by-name interface walk.
759+
if (ClrTypeUtilities.ImplementsInterfaceByName(slice.ClrType, targetInterface.ClrType))
760+
{
761+
return true;
762+
}
763+
764+
if (targetInterface is not ImportedTypeSymbol imported
765+
|| imported.OpenDefinition is null
766+
|| imported.TypeArguments.IsDefaultOrEmpty)
767+
{
768+
return false;
769+
}
770+
771+
// Cross-context fallback: walk the slice's backing CLR array
772+
// interfaces and match each generic argument against the symbolic
773+
// TypeArguments by ClrType FullName, which is assembly-qualifier
774+
// free for leaf types (System.Int32, System.String, …) and so
775+
// compares correctly across reflection contexts.
776+
var openDef = imported.OpenDefinition;
777+
foreach (var iface in slice.ClrType.GetInterfaces())
778+
{
779+
if (!iface.IsGenericType)
780+
{
781+
continue;
782+
}
783+
784+
if (!string.Equals(
785+
iface.GetGenericTypeDefinition().FullName,
786+
openDef.FullName,
787+
StringComparison.Ordinal))
788+
{
789+
continue;
790+
}
791+
792+
var ifaceArgs = iface.GetGenericArguments();
793+
if (ifaceArgs.Length != imported.TypeArguments.Length)
794+
{
795+
continue;
796+
}
797+
798+
var allMatch = true;
799+
for (var i = 0; i < ifaceArgs.Length; i++)
800+
{
801+
var symbolic = imported.TypeArguments[i];
802+
if (symbolic?.ClrType is null
803+
|| !string.Equals(ifaceArgs[i].FullName, symbolic.ClrType.FullName, StringComparison.Ordinal))
804+
{
805+
allMatch = false;
806+
break;
807+
}
808+
}
809+
810+
if (allMatch)
811+
{
812+
return true;
813+
}
814+
}
815+
816+
return false;
817+
}
736818
}

src/Core/CodeAnalysis/Emit/MethodBodyEmitter.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,58 @@ private static bool IsReferenceCompatible(TypeSymbol a, TypeSymbol b)
532532
return true;
533533
}
534534

535+
// Issue #821: cross-context slice-to-constructed-interface widening
536+
// where the target is an <see cref="ImportedTypeSymbol"/> whose
537+
// <c>ClrType</c> is the type-erased open-definition form (because
538+
// <c>MakeGenericType</c> at substitution time could not produce a
539+
// closed CLR type — the open def lives in a MetadataLoadContext while
540+
// the arguments live in the host runtime). Match the slice's backing
541+
// <c>T[]</c> interfaces against the open definition's full name and
542+
// the symbolic <c>TypeArguments</c> by leaf-FullName so the CLR
543+
// no-op upcast classifies correctly. Mirrors the binder fallback in
544+
// <see cref="Conversion.SliceImplementsInterface"/>.
545+
if (a is SliceTypeSymbol aSlice
546+
&& aSlice.ClrType != null
547+
&& b is ImportedTypeSymbol bImported
548+
&& bImported.OpenDefinition is { } bOpenDef
549+
&& !bImported.TypeArguments.IsDefaultOrEmpty)
550+
{
551+
foreach (var iface in aSlice.ClrType.GetInterfaces())
552+
{
553+
if (!iface.IsGenericType
554+
|| !string.Equals(
555+
iface.GetGenericTypeDefinition().FullName,
556+
bOpenDef.FullName,
557+
StringComparison.Ordinal))
558+
{
559+
continue;
560+
}
561+
562+
var ifaceArgs = iface.GetGenericArguments();
563+
if (ifaceArgs.Length != bImported.TypeArguments.Length)
564+
{
565+
continue;
566+
}
567+
568+
var allMatch = true;
569+
for (var i = 0; i < ifaceArgs.Length; i++)
570+
{
571+
var symbolic = bImported.TypeArguments[i];
572+
if (symbolic?.ClrType is null
573+
|| !string.Equals(ifaceArgs[i].FullName, symbolic.ClrType.FullName, StringComparison.Ordinal))
574+
{
575+
allMatch = false;
576+
break;
577+
}
578+
}
579+
580+
if (allMatch)
581+
{
582+
return true;
583+
}
584+
}
585+
}
586+
535587
return false;
536588
}
537589

src/Core/CodeAnalysis/Emit/ReflectionMetadataEmitter.cs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5445,6 +5445,59 @@ private static bool TryUnify(TypeSymbol formal, TypeSymbol actual, TypeParameter
54455445
}
54465446
}
54475447

5448+
// Issue #821: when the formal is a constructed CLR generic
5449+
// interface that the actual's backing array satisfies — e.g.
5450+
// `IEnumerable[T]` / `IList[T]` / `ICollection[T]` /
5451+
// `IReadOnlyList[T]` (any interface implemented by `T[]`) — and
5452+
// the actual is a `[]T` slice or `[N]T` fixed-length array,
5453+
// bridge their generic arguments by locating the matching
5454+
// interface instantiation on the actual's backing CLR `T[]` and
5455+
// recursing into the element-type slot. Mirrors the binder's
5456+
// slice-to-interface classifier (#570) and the
5457+
// `sequence[T]`-vs-slice/array arm above (#774/#814) at the
5458+
// static-method / free-function argument-slot inference path so
5459+
// generic-method-spec construction can recover `T` from a
5460+
// slice argument when the type parameter only appears in an
5461+
// interface-typed formal parameter (no `T` in the return).
5462+
if (formal is ImportedTypeSymbol formalImported
5463+
&& formalImported.ClrType is { IsInterface: true, IsGenericType: true } formalIface
5464+
&& !formalImported.TypeArguments.IsDefaultOrEmpty
5465+
&& (actual is SliceTypeSymbol || actual is ArrayTypeSymbol)
5466+
&& actual?.ClrType is { IsArray: true } actualClrArray)
5467+
{
5468+
Type matched = null;
5469+
foreach (var iface in actualClrArray.GetInterfaces())
5470+
{
5471+
if (iface.IsGenericType
5472+
&& ClrTypeUtilities.AreSame(
5473+
iface.GetGenericTypeDefinition(),
5474+
formalIface.GetGenericTypeDefinition()))
5475+
{
5476+
matched = iface;
5477+
break;
5478+
}
5479+
}
5480+
5481+
if (matched != null)
5482+
{
5483+
var matchedArgs = matched.GetGenericArguments();
5484+
if (formalImported.TypeArguments.Length == matchedArgs.Length)
5485+
{
5486+
for (int i = 0; i < formalImported.TypeArguments.Length; i++)
5487+
{
5488+
if (TryUnify(
5489+
formalImported.TypeArguments[i],
5490+
TypeSymbol.FromClrType(matchedArgs[i]),
5491+
tp,
5492+
out inferred))
5493+
{
5494+
return true;
5495+
}
5496+
}
5497+
}
5498+
}
5499+
}
5500+
54485501
var formalArgs = GetGenericTypeArguments(formal);
54495502
var actualArgs = GetGenericTypeArguments(actual);
54505503
if (!formalArgs.IsDefaultOrEmpty && !actualArgs.IsDefaultOrEmpty
@@ -6274,7 +6327,7 @@ private bool TryCreateMemberReferenceForConstructedSymbolicContainer(
62746327
// null ClrType (issue #774), or an AsyncSequenceTypeSymbol with
62756328
// null ClrType.
62766329
var symbolicView = ImportedTypeSymbol.GetConstructed(
6277-
openDefinition.MakeGenericType(GetErasedObjectArgs(openDefinition)),
6330+
openDefinition.MakeGenericType(this.GetErasedObjectArgs(openDefinition)),
62786331
openDefinition,
62796332
typeArguments);
62806333

@@ -6380,18 +6433,39 @@ private static bool TryNormalizeToSymbolicContainer(
63806433
}
63816434
}
63826435

6383-
private static Type[] GetErasedObjectArgs(Type openDefinition)
6436+
// Issue #821: choose the right erased `object` for an open generic
6437+
// definition's MakeGenericType call. The open def may live in a
6438+
// MetadataLoadContext (reference-pack assemblies); passing a live
6439+
// `typeof(object)` to its MakeGenericType raises ArgumentException with
6440+
// "type was not loaded by the MetadataLoadContext that loaded the
6441+
// generic type or method." Use `emitCtx.CoreObjectType`, which is the
6442+
// System.Object resolved through the active reference context, when the
6443+
// open def lives outside the host runtime.
6444+
private Type[] GetErasedObjectArgs(Type openDefinition)
63846445
{
63856446
var parameters = openDefinition.GetGenericArguments();
63866447
var result = new Type[parameters.Length];
6448+
var coreObject = ChooseErasedObjectType(openDefinition);
63876449
for (var i = 0; i < parameters.Length; i++)
63886450
{
6389-
result[i] = typeof(object);
6451+
result[i] = coreObject;
63906452
}
63916453

63926454
return result;
63936455
}
63946456

6457+
private Type ChooseErasedObjectType(Type openDefinition)
6458+
{
6459+
// Same context as the open def → cheap path.
6460+
var hostObject = typeof(object);
6461+
if (openDefinition?.Assembly == hostObject.Assembly)
6462+
{
6463+
return hostObject;
6464+
}
6465+
6466+
return this.emitCtx.CoreObjectType ?? hostObject;
6467+
}
6468+
63956469
private static MethodInfo ResolveMethodOnOpenDefinition(Type openDefinition, MethodInfo method)
63966470
{
63976471
if (openDefinition == null)

0 commit comments

Comments
 (0)