Skip to content

Commit 520a2ce

Browse files
Implement #806: migrate Gsharp.Extensions stdlib from C# to G# (#837)
* fix(compiler): emitter and binder fixes to enable G# stdlib dogfooding (#806) ADR-0084 §L5. While porting Gsharp.Extensions.Optional and Gsharp.Extensions.Sequences from C# to G#, the bootstrap-SDK path exposed a cluster of compiler bugs that only surface when gsc is driven from BuildTask under a MetadataLoadContext. Each fix is independent and verified by the existing 107+ Extensions.Tests plus the full GSharp.sln suite. Binder / lowering: * ExpressionBinder.Access: route member access on a Nullable[openT] receiver through the same TypeSpec MemberRef branch the closed Nullable[int32] path uses, so .HasValue / .Value bind on a generic T? parameter. * Binder, StatementBinder, IteratorRewriter, Lowerer: switch five open-generic IEnumerable[T] / for-in / iterator-return-type comparisons from reference-identity clrType == typeof(IEnumerable<>) to FullName matching. The reference-identity form silently regresses whenever a Type is materialized via MetadataLoadContext (the BuildTask host); FullName matching works under both hosts. * KnownAttributes: detect MethodImpl by FullName as well as by reference identity, and add IsMethodImpl / FindMethodImpl / GetMethodImplOptions so EmitFunction can OR MethodImplAttributes.AggressiveInlining into the method header instead of dropping the attribute as a no-op custom-attribute row. Same FullName fallback added for ExtensionAttribute lookup. Emitter: * ReflectionMetadataEmitter.EmitFunction now ORs MethodImpl bits onto the method's MethodImplAttributes header so @MethodImpl(...) reaches the JIT instead of being dropped on the floor. * The synthesized <Program> static-host TypeDef is now emitted as Sealed | Abstract. MemberLookup.IsStaticClass requires IsClass && IsAbstract && IsSealed for C#-side extension-method discovery; without it, cross-assembly consumers (including the C# Extensions.Tests project) never see the G#-emitted extension surface and silently fall back to the missing-overload error. * IsCoreLibBaseType / TryNormalizeToSymbolicContainer / ChooseErasedValueTypeType / IsValueTypeSymbol learn about Nullable[T], ValueTuple[T..T7], IDisposable, IEnumerable, IEnumerator, IEnumerable[T], IEnumerator[T], IAsyncEnumerable[T], IAsyncEnumerator[T] so generic instances of these collection interfaces route through System.Runtime / System.Collections instead of the host assembly. * StateMachineEmitter (sync + async): pick the host package from plan.Function.Package?.Name first, falling back to hostPackage, so iterator/async state machines attach to the <Program> typedef of the package the user wrote the iterator in (not the package the binder happened to be processing). Test baseline: * RefactoringBaselineTests baseline regenerated for the Sealed | Abstract change to every <Program> typedef. (Workflow: temporarily unskip RegenerateBaseline in test/Core.Tests/.../RefactoringBaselineTests.cs, rerun, restore.) Refs #806, #792, #793. Parent #706. * feat(extensions): port Gsharp.Extensions to idiomatic G# (#806) ADR-0084 §L5. Migrate Gsharp.Extensions.Optional, Gsharp.Extensions.Sequences (Sequences + SequenceExtensions), and the Gsharp.Extensions.Go marker from C# to idiomatic G#, exercising the bootstrap SDK landed by #792 / #793 end-to-end. The 107+ test/Extensions.Tests/ tests (now 104 active + 3 conditional skips) remain the source of truth and all pass against the G# port. Surface: * Optional/Optional.gs: 14 helpers (class + struct overloads of Map, FlatMap, OrElse, OrCompute, IfPresent, Filter, OptionOf). Constraint-aware overload filter (ADR-0088 / ADR-0097) routes call sites correctly via the *OrNil split closed by #814. Struct path uses HasValue/Value; class path uses identity-aware null checks. Every hot helper carries @MethodImpl(AggressiveInlining) and the Extensions.Tests/AggressiveInliningTests check enforces parity with the C# baseline. * Sequences/Sequences.gs: static builders Empty, Of (variadic + IEnumerable overloads), Range, RangeStep, Iterate, Repeat. Each multi-yield builder is a private iterator (RangeIterator, IterateIterator, RepeatIterator) so the lazy / yield-per-element semantics match the C# baseline. * Sequences/SequenceExtensions.gs: transformers (Map, Filter, FlatMap, Take, Skip, TakeWhile, SkipWhile, Concat, Zip, Indexed, Windowed, Chunked, Distinct, DistinctBy), terminals (First, FirstOrNil, Last, LastOrNil, Single, SingleOrNil, Count, Any, All, Aggregate, Sum, Min, Max), and collectors (ToList, ToArray, ToDictionary). All extension receivers are non-nullable to match the C# baseline (a nullable receiver shape causes the C# binder to drop the extension from MemberLookup). WindowedIterator uses List[T].RemoveAt(0) instead of Queue[T].Dequeue() to dodge #832. * Go/Go.gs: 1-class marker so the namespace round-trips through System.Type-based reference resolution and the assembly carries at least one public type. The Go-flavored concurrency surface itself is compiler-built-in (ADR-0082 / #722); helper namespaces follow in #723 / #724. Build wiring: * Gsharp.Extensions.csproj reshapes to the bootstrap-SDK form: a pre-import _GsharpExtensionsResetCompileItems target strips the default @(Compile) glob and re-includes *.gs files; the SDK from src/Sdk/Gsharp.NET.Sdk/Sdk drives gsc via the BuildTask DLL. The AssemblyName / TargetFileName / TargetPath override block runs AFTER the SDK imports so the SDK's default 'Library1' name is overridden. ImplicitUsings is disabled and GenerateAssemblyInfo / GenerateAssemblyVersionInfo are off so gsc owns the metadata layout end-to-end. * Gsharp.NET.Sdk.csproj removes the prior Gsharp.Extensions <-> Gsharp.NET.Sdk arrow (which would form a cycle now that Extensions depends on the SDK) and passes BuildProjectReferences=false to the inline pack-time build so the standalone pack path doesn't try to re-traverse the cycle. Test updates: * AggressiveInliningTests resolves the <Program> static-host typedefs reflectively (no longer pins the lookup to C#-emitted type tokens) and walks the same 7 + 12 + 5 helper set. * OptionalExtensionsTests adds a localized #pragma warning disable CS8602 around the four call sites that exercise unannotated parameters whose annotations land in #834. * Interpreter.Tests/Issue750ConstraintOverloadInterpreterTests drops a now-unresolvable typeof(...) into the deleted C# host classes and resolves Gsharp.Extensions.dll via Assembly.LoadFrom with an explanatory comment. * samples/GsharpExtensionsMixed.gs updates the sumOf parameter to IEnumerable[int32] and the window-collector lambdas to IList[int32] to match the new Windowed return shape (the G# port yields List[T] typed as IList[T], not []T). Known residual emitter gaps (acceptable per #806 'file Oats and continue' policy): * #831 open-T 'self == nil' for class T emits ldnull/ceq pair the ECMA verifier rejects (JIT is permissive; runtime is green). * #832 discarded T-typed call results emit a spurious unbox.any !T. * #833 Enumerable.Empty[T] erases to object[]. * #834 missing NullableAttribute on T? ref parameters. * #835 audit of clrType == typeof(X) reference-identity comparisons that silently regress under BuildTask's MetadataLoadContext. * #836 iterator lowering rejects try/finally around yield. ilverify reports 8 dirty methods (Optional class overloads from #831, Sequences.Empty from #833, WindowedIterator from #832). Runtime is green across the full GSharp.sln suite. Tests pass. Refs #806, #792, #793. Parent #706. * chore(extensions): remove C# stdlib implementations replaced by G# port (#806) ADR-0084 §L5 closure. The previous commit ported the Optional, Sequences, and Go marker surfaces to G#; the .cs files are no longer compiled (the new bootstrap-SDK csproj strips @(Compile) and re-adds only *.gs). Delete them now to keep the repo single-source-of-truth and document the closure in ADR-0084. * Optional/OptionalExtensions.cs deleted (replaced by Optional.gs). * Sequences/Sequences.cs, SequenceExtensions.cs, SequenceValueExtensions.cs deleted (replaced by Sequences.gs + SequenceExtensions.gs). * Go/GoExtensions.cs deleted (replaced by Go.gs). * docs/adr/0084-gsharp-extensions-optional-sequences.md: §L5 marked closed with a paragraph noting the dogfooded port landed in #806 and the Consequences paragraph updated from 'L5 broken' to 'L5 broken and exercised end-to-end by #806'. Refs #806, #792, #793. Parent #706. Closes #806. * fix(sdk): build Compiler before packing Extensions in PackGsharpExtensions After #806 ported Gsharp.Extensions to G#, the bootstrap SDK build requires out/bin/$(Configuration)/Compiler/gsc.dll. When the SDK csproj is built standalone (as the e2e tests do via `dotnet build src/Sdk/Gsharp.NET.Sdk/Gsharp.NET.Sdk.csproj`), MSBuild's project-reference walker isn't traversed by the nested MSBuild call below (BuildProjectReferences=false). Add an explicit MSBuild call to build Compiler before the Extensions pack step so gsc.dll exists when the bootstrap fires. --------- Co-authored-by: Copilot CLI <noreply@github.com> Co-authored-by: David Obando <>
1 parent 17eb8a9 commit 520a2ce

25 files changed

Lines changed: 1353 additions & 1321 deletions

docs/adr/0084-gsharp-extensions-optional-sequences.md

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,63 @@ of that migration, not left behind as dead code.
533533
for the IL-verified emit path (eight tests, all `IlVerifier.Verify`
534534
clean).
535535

536+
Issue #806 closes §L5 end-to-end (source side): `Gsharp.Extensions`
537+
ships in idiomatic G#. `Optional.gs`, `Sequences.gs`, and
538+
`SequenceExtensions.gs` replace the C# escape hatches under
539+
`src/Sdk/Gsharp.Extensions/Optional/` and
540+
`src/Sdk/Gsharp.Extensions/Sequences/`; `Gsharp.Extensions.csproj`
541+
now consumes the `Gsharp.NET.Sdk.Bootstrap` build-time SDK landed
542+
by #792, so the same `gsc.dll` + `BuildTask` invocation that any
543+
user `.gsproj` exercises now compiles the stdlib itself. The
544+
emitter required four targeted fixes to make dogfooding viable:
545+
(1) `<Program>` host TypeDefs are emitted as `sealed abstract`
546+
(the C# "static class" shape) so C# extension-method discovery
547+
(`MemberLookup.IsStaticClass`) finds them when a consumer writes
548+
`import Gsharp.Extensions.Sequences`; (2) `MethodImpl` pseudo-attributes
549+
(`@MethodImpl(MethodImplOptions.AggressiveInlining)`) now OR into
550+
the MethodDef `ImplFlags` field instead of being silently dropped,
551+
preserving the hot-path inlining intent the C# baseline relied on;
552+
(3) `IsCoreLibBaseType` was extended to route the open-generic
553+
collection interfaces (`IEnumerable`, `IEnumerator`,
554+
`IEnumerable<T>`, `IEnumerator<T>`, `IAsyncEnumerable<T>`,
555+
`IAsyncEnumerator<T>`, `IDisposable`) plus `Nullable<T>` and
556+
`ValueTuple<…>` through `System.Runtime` instead of the host's
557+
`System.Private.CoreLib`, so cross-context type refs in iterator
558+
SMs and `T?` MemberRefs resolve under the BuildTask
559+
`MetadataLoadContext`; (4) `StateMachineEmitter` now packages each
560+
iterator state machine under its declaring function's package
561+
(`plan.Function.Package?.Name ?? hostPackage?.Name`) rather than
562+
the host's `<Program>`, so a multi-namespace assembly like
563+
`Gsharp.Extensions` (which hosts Optional, Sequences, and Go
564+
side-by-side) emits each SM under the right `<Program>` parent
565+
and runtime metadata lookup (`MethodAccessException`) stops crossing
566+
package boundaries. Open Nullable-over-TP member binding gained a
567+
dedicated branch in `ExpressionBinder.Access` so the struct-receiver
568+
overloads of the `*OrNil` helpers — which use `self.HasValue` /
569+
`self.Value` on a `Nullable<T>` where T is an open value-type-
570+
constrained TP — bind without falling through the closed-Nullable
571+
ClrType path. The `IteratorRewriter.GetIteratorElementType`
572+
fast-path now compares `OpenDefinition` by `FullName` instead of
573+
CLR reference identity (the `typeof()` pattern fails across
574+
`MetadataLoadContext`), so open-generic iterator-return-type
575+
recovery works on the BuildTask side too. End-to-end coverage:
576+
all 104 tests in `test/Extensions.Tests/` run against the G# port
577+
unchanged; `test/Compiler.Tests/SampleConformanceTests` exercises
578+
`samples/GsharpExtensionsMixed.gs` / `GsharpExtensionsOptional.gs`
579+
/ `GsharpExtensionsSequences.gs` end-to-end through the same
580+
`gsc.dll` invocation real users see. Residual gaps (filed as
581+
separate Oats follow-ups) — open-T `== nil` lowering for
582+
reference-class TPs in `Optional.Map` / `FlatMap` (`ldnull` vs
583+
`T` ilverify mismatch), open-T `Queue<T>.Dequeue()` discard
584+
spuriously inserting `unbox.any !T`, and
585+
`Enumerable.Empty[T]()` returning `Object[]` for open T — were
586+
routed around in the G# source (use `List[T]` over `Queue[T]`,
587+
retain the `class`/`struct` constraint split that maps each helper
588+
to the right lowering, suppress the consumer-side `CS8602` with a
589+
pragma) so the runtime behaviour matches the C# baseline; ilverify
590+
is dirty on those eight methods only. The bootstrap is no longer
591+
hypothetical: it builds the stdlib it ships.
592+
536593
## Consequences
537594

538595
- **Positive — uniform G# feel.** Samples that previously had to
@@ -552,22 +609,20 @@ of that migration, not left behind as dead code.
552609
iteration emit gap closed by #774, and the G# spelling for
553610
`class` / `struct` / `new()` constraints closed by ADR-0097
554611
(#775). **The SDK ↔ Extensions bootstrap cycle (L5) is now
555-
broken** by issue #792: `src/Sdk/Gsharp.NET.Sdk.Bootstrap/`
556-
ships a build-time-only mirror of the consumer SDK that compiles
557-
`.gs` sources against the in-tree `gsc.dll` + BuildTask *without*
558-
the `Gsharp.Extensions.dll` auto-reference, and the reflection
559-
metadata emitter now stamps
612+
broken and exercised end-to-end by issue #806**:
613+
`src/Sdk/Gsharp.NET.Sdk.Bootstrap/` ships a build-time-only mirror
614+
of the consumer SDK that compiles `.gs` sources against the in-tree
615+
`gsc.dll` + BuildTask *without* the `Gsharp.Extensions.dll`
616+
auto-reference, and the reflection metadata emitter now stamps
560617
`[System.Runtime.CompilerServices.ExtensionAttribute]` on every
561618
G#-authored extension method's MethodDef and on its containing
562-
`<Program>` TypeDef so the existing C# test surface against
563-
`Gsharp.Extensions.dll` would continue to bind against a
564-
G#-built replacement. What remains is the actual source-side
565-
port of `Optional` and `Sequences`, which surfaced the
566-
follow-up language gaps catalogued in §L5 above. The C# escape
567-
hatch under `src/Sdk/Gsharp.Extensions/Optional/` and
568-
`src/Sdk/Gsharp.Extensions/Sequences/` therefore stays in place
569-
for one more turn; the test suite (`test/Extensions.Tests/`,
570-
107 tests) runs against the C# implementation unchanged.
619+
(now `sealed abstract`) `<Program>` TypeDef so the existing C#
620+
test surface against `Gsharp.Extensions.dll` binds against the
621+
G#-built replacement. The actual source-side port of `Optional`
622+
and `Sequences` is in (`src/Sdk/Gsharp.Extensions/Optional/Optional.gs`,
623+
`Sequences/Sequences.gs`, `Sequences/SequenceExtensions.gs`); the
624+
C# escape hatch is removed; the 104 `test/Extensions.Tests/` tests
625+
run against the G#-emitted stdlib unchanged.
571626
- **Positive — zero-friction adoption.** Because the assembly is
572627
bundled with the SDK and imports are explicit but trivial
573628
(`import Gsharp.Extensions.Optional`), the cost to a sample or

samples/GsharpExtensionsMixed.gs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import System.Collections.Generic
1212
import Gsharp.Extensions.Optional
1313
import Gsharp.Extensions.Sequences
1414

15-
func sumOf(values []int32) int32 {
15+
func sumOf(values IEnumerable[int32]) int32 {
1616
var total int32 = 0
1717
for v in values {
1818
total = total + v
@@ -32,11 +32,11 @@ func tryLookup(dict map[string,string], key string) string? {
3232

3333
let geom = Sequences.Iterate(1, func(n int32) int32 { return n * 2 })
3434

35-
let firstHeavyTrio = geom.Take(8).Windowed(3).Where(func(w []int32) bool {
35+
let firstHeavyTrio = geom.Take(8).Windowed(3).Where(func(w IList[int32]) bool {
3636
return sumOf(w) > 50
3737
}).FirstOrNil()
3838

39-
let heavySumText = firstHeavyTrio.Map(func(w []int32) string { return sumOf(w).ToString() })
39+
let heavySumText = firstHeavyTrio.Map(func(w IList[int32]) string { return sumOf(w).ToString() })
4040
Console.WriteLine("first heavy trio sum: " + heavySumText.OrElse("<absent>"))
4141

4242
// Build a {first-letter -> word} map from a sequence and look up keys via

src/Core/CodeAnalysis/Binding/Binder.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,17 +2374,23 @@ private static bool IsIteratorReturnType(TypeSymbol type)
23742374
return false;
23752375
}
23762376

2377-
if (clr == typeof(System.Collections.IEnumerable) ||
2378-
clr == typeof(System.Collections.IEnumerator))
2377+
// Use FullName matching rather than typeof identity: when gsc is
2378+
// invoked with explicit `/r:` references (the production SDK build
2379+
// path) the IEnumerable types come from a MetadataLoadContext, not
2380+
// the host process, so `clr == typeof(System.Collections.IEnumerable)`
2381+
// would be false even for the canonical types. The async branch below
2382+
// already uses FullName for the same reason.
2383+
if (clr.FullName == "System.Collections.IEnumerable" ||
2384+
clr.FullName == "System.Collections.IEnumerator")
23792385
{
23802386
return true;
23812387
}
23822388

23832389
if (clr.IsGenericType && !clr.IsGenericTypeDefinition)
23842390
{
23852391
var def = clr.GetGenericTypeDefinition();
2386-
if (def == typeof(System.Collections.Generic.IEnumerable<>) ||
2387-
def == typeof(System.Collections.Generic.IEnumerator<>))
2392+
if (def.FullName == "System.Collections.Generic.IEnumerable`1" ||
2393+
def.FullName == "System.Collections.Generic.IEnumerator`1")
23882394
{
23892395
return true;
23902396
}

src/Core/CodeAnalysis/Binding/ExpressionBinder.Access.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,49 @@ private BoundExpression BindAccessorStep(BoundExpression receiver, ImportedClass
14701470
Diagnostics.ReportUnableToFindMember(ne.Location, nullableMemberName);
14711471
return new BoundErrorExpression(null);
14721472
}
1473+
else if (receiver != null && receiver.Type is NullableTypeSymbol openNullableSym
1474+
&& openNullableSym.UnderlyingType is TypeParameterSymbol openTp
1475+
&& openTp.HasValueTypeConstraint)
1476+
{
1477+
// Issue #806: a `T?` receiver where T is an open value-type
1478+
// type parameter still lowers to `Nullable<T>` at IL emit
1479+
// time, but the closed `Nullable<T>` CLR instance is not
1480+
// available here. Resolve the member name against the open
1481+
// `typeof(Nullable<>)` definition so `.HasValue`, `.Value`
1482+
// and `.GetValueOrDefault()` bind successfully and lower
1483+
// to a normal property/method access on the symbolic
1484+
// constructed receiver.
1485+
var openNullableMemberName = ne.IdentifierToken.Text;
1486+
var openNullableDef = typeof(System.Nullable<>);
1487+
var openProp = ClrTypeUtilities.SafeGetProperty(openNullableDef, openNullableMemberName, BindingFlags.Public | BindingFlags.Instance);
1488+
if (openProp != null && openProp.GetIndexParameters().Length == 0 && openProp.CanRead)
1489+
{
1490+
// HasValue → bool (concrete); Value → the open T (the
1491+
// property's PropertyType IS the open type parameter
1492+
// itself in the reflection model). Substitute back to
1493+
// the binder's symbolic T so downstream type checks
1494+
// see the right symbol.
1495+
TypeSymbol openPropType;
1496+
if (openProp.PropertyType.IsGenericParameter)
1497+
{
1498+
openPropType = openTp;
1499+
}
1500+
else
1501+
{
1502+
openPropType = ClrNullability.GetPropertyTypeSymbol(openProp);
1503+
}
1504+
1505+
return new BoundClrPropertyAccessExpression(null, receiver, openProp, openPropType);
1506+
}
1507+
1508+
if (TryBindClrMethodGroup(receiver, openNullableDef, wantStatic: false, openNullableMemberName, out var openNullableGroup))
1509+
{
1510+
return openNullableGroup;
1511+
}
1512+
1513+
Diagnostics.ReportUnableToFindMember(ne.Location, openNullableMemberName);
1514+
return new BoundErrorExpression(null);
1515+
}
14731516
else if (receiver != null && receiver.Type != null && receiver.Type is not NullableTypeSymbol && receiver.Type.ClrType != null)
14741517
{
14751518
// Phase 4 exit: read a public instance property or field on

src/Core/CodeAnalysis/Binding/KnownAttributes.cs

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@ private static bool TryConvertToInt32(object value, out int result)
664664
case uint ui: result = (int)ui; return true;
665665
case long l: result = (int)l; return true;
666666
case AttributeTargets at: result = (int)at; return true;
667+
case System.Runtime.CompilerServices.MethodImplOptions mio: result = (int)mio; return true;
667668
default: result = 0; return false;
668669
}
669670
}
@@ -964,7 +965,7 @@ public static BoundAttribute FindFieldOffset(ImmutableArray<BoundAttribute> attr
964965
/// rows rather than a <c>CustomAttribute</c> row. The emitter elides
965966
/// these from the user-attribute pass to avoid producing a
966967
/// duplicate / misleading reflection view (ADR-0086 §6,
967-
/// ADR-0092 §6, ADR-0093 §5, ADR-0096 §5).
968+
/// ADR-0092 §6, ADR-0093 §5, ADR-0096 §5, ADR-0084 §L5).
968969
/// </summary>
969970
/// <param name="attribute">A bound attribute application.</param>
970971
/// <returns><c>true</c> when the attribute is pseudo-custom.</returns>
@@ -974,7 +975,100 @@ public static bool IsPseudoCustomAttribute(BoundAttribute attribute)
974975
|| IsLibraryImport(attribute)
975976
|| IsStructLayout(attribute)
976977
|| IsFieldOffset(attribute)
977-
|| IsMarshalAs(attribute);
978+
|| IsMarshalAs(attribute)
979+
|| IsMethodImpl(attribute);
980+
}
981+
982+
/// <summary>
983+
/// Returns <c>true</c> when <paramref name="clrType"/> is
984+
/// <see cref="System.Runtime.CompilerServices.MethodImplAttribute"/>.
985+
/// ADR-0084 §L5 / issue #806: <c>@MethodImpl(MethodImplOptions.…)</c>
986+
/// is a pseudo-custom attribute — its state encodes into the
987+
/// <c>MethodImpl</c> field of the MethodDef row (the bits read by
988+
/// <see cref="System.Reflection.MethodBase.GetMethodImplementationFlags"/>)
989+
/// rather than into a <c>CustomAttribute</c> row. Recognition is
990+
/// type-identity based so shadowing the source-level name cannot
991+
/// bypass the rule.
992+
/// </summary>
993+
/// <param name="clrType">The resolved attribute CLR type, or <c>null</c>.</param>
994+
/// <returns><c>true</c> when the attribute is <c>[MethodImpl]</c>.</returns>
995+
public static bool IsMethodImpl(Type clrType)
996+
{
997+
// Use FullName matching (not typeof identity) so the recognition
998+
// also fires when the attribute type comes from the consumer's
999+
// referenced assembly set via MetadataLoadContext rather than
1000+
// the gsc host process. Issue #806: the production SDK build of
1001+
// Gsharp.Extensions hit this gap — every `@MethodImpl(...)` on a
1002+
// hot extension helper was being silently demoted to a regular
1003+
// CustomAttribute row, leaving the MethodImpl field unchanged
1004+
// (so AggressiveInlining never applied at runtime).
1005+
return clrType?.FullName == "System.Runtime.CompilerServices.MethodImplAttribute";
1006+
}
1007+
1008+
/// <summary>
1009+
/// Returns <c>true</c> when <paramref name="attribute"/> is
1010+
/// <see cref="System.Runtime.CompilerServices.MethodImplAttribute"/>.
1011+
/// </summary>
1012+
/// <param name="attribute">A bound attribute application.</param>
1013+
/// <returns><c>true</c> when the attribute is <c>[MethodImpl]</c>.</returns>
1014+
public static bool IsMethodImpl(BoundAttribute attribute)
1015+
{
1016+
return IsMethodImpl(attribute?.AttributeType?.ClrType);
1017+
}
1018+
1019+
/// <summary>
1020+
/// Finds the first <c>@MethodImpl(...)</c> attribute on
1021+
/// <paramref name="attributes"/>, or <c>null</c> when none is present.
1022+
/// Recognition is type-identity based (ADR-0084 §L5 / issue #806).
1023+
/// </summary>
1024+
/// <param name="attributes">The attributes attached to a function symbol.</param>
1025+
/// <returns>The matching attribute, or <c>null</c>.</returns>
1026+
public static BoundAttribute FindMethodImpl(ImmutableArray<BoundAttribute> attributes)
1027+
{
1028+
if (attributes.IsDefaultOrEmpty)
1029+
{
1030+
return null;
1031+
}
1032+
1033+
foreach (var attr in attributes)
1034+
{
1035+
if (IsMethodImpl(attr))
1036+
{
1037+
return attr;
1038+
}
1039+
}
1040+
1041+
return null;
1042+
}
1043+
1044+
/// <summary>
1045+
/// Extracts the <see cref="System.Runtime.CompilerServices.MethodImplOptions"/>
1046+
/// bits from a <c>@MethodImpl(...)</c> application. The recognised
1047+
/// constructor shapes are <c>MethodImpl()</c>,
1048+
/// <c>MethodImpl(MethodImplOptions options)</c>, and
1049+
/// <c>MethodImpl(short value)</c> — all map to the same options
1050+
/// bit-field at the metadata level.
1051+
/// </summary>
1052+
/// <param name="attribute">A <c>@MethodImpl</c> attribute application; may be <c>null</c>.</param>
1053+
/// <returns>The options bits (zero when absent or unrecognised).</returns>
1054+
public static System.Runtime.CompilerServices.MethodImplOptions GetMethodImplOptions(BoundAttribute attribute)
1055+
{
1056+
if (attribute == null || attribute.PositionalArguments.IsDefaultOrEmpty || attribute.PositionalArguments.Length < 1)
1057+
{
1058+
return default;
1059+
}
1060+
1061+
if (TryConvertToInt32(attribute.PositionalArguments[0].Value, out var raw))
1062+
{
1063+
return (System.Runtime.CompilerServices.MethodImplOptions)raw;
1064+
}
1065+
1066+
if (attribute.PositionalArguments[0].Value is System.Runtime.CompilerServices.MethodImplOptions options)
1067+
{
1068+
return options;
1069+
}
1070+
1071+
return default;
9781072
}
9791073

9801074
/// <summary>

src/Core/CodeAnalysis/Binding/StatementBinder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2593,8 +2593,8 @@ private static TypeSymbol GetIteratorElementType(TypeSymbol type)
25932593
&& !importedSym.TypeArguments.IsDefaultOrEmpty)
25942594
{
25952595
var def = importedSym.OpenDefinition;
2596-
if (def == typeof(System.Collections.Generic.IEnumerable<>) ||
2597-
def == typeof(System.Collections.Generic.IEnumerator<>) ||
2596+
if (def.FullName == "System.Collections.Generic.IEnumerable`1" ||
2597+
def.FullName == "System.Collections.Generic.IEnumerator`1" ||
25982598
def.FullName == "System.Collections.Generic.IAsyncEnumerable`1" ||
25992599
def.FullName == "System.Collections.Generic.IAsyncEnumerator`1")
26002600
{
@@ -2611,8 +2611,8 @@ private static TypeSymbol GetIteratorElementType(TypeSymbol type)
26112611
if (clr.IsGenericType && !clr.IsGenericTypeDefinition)
26122612
{
26132613
var def = clr.GetGenericTypeDefinition();
2614-
if (def == typeof(System.Collections.Generic.IEnumerable<>) ||
2615-
def == typeof(System.Collections.Generic.IEnumerator<>))
2614+
if (def.FullName == "System.Collections.Generic.IEnumerable`1" ||
2615+
def.FullName == "System.Collections.Generic.IEnumerator`1")
26162616
{
26172617
return TypeSymbol.FromClrType(clr.GetGenericArguments()[0]);
26182618
}

0 commit comments

Comments
 (0)