Decide extension method syntax in CallBuilder - #4160
Open
siegfriedpammer wants to merge 4 commits into
Open
siegfriedpammer wants to merge 4 commits into
siegfriedpammer wants to merge 4 commits into
Conversation
C# resolves an extension method one namespace at a time, starting at the namespace the call is written in and working outwards through the enclosing ones before it considers any import. A library that defines GetCustomAttributes(this MemberInfo) next to its callers therefore beats System.Reflection's identically shaped one, and the call compiles. ExpressionBuilder's resolver had a flat scope: the namespace superset was one undifferentiated list of imports, so the two competed as equals and the call looked ambiguous. The deleted AST transform did not have this problem because it nested its scope into the current type's namespace before resolving anything. The scope is memoized per namespace on the DecompileRun, because the set of extension methods a scope can reach is computed once per scope and an ExpressionBuilder exists per function. Assisted-by: Claude:claude-opus-5:Claude Code
An extension method reference binds its receiver as the target, so the delegate it is converted to has one parameter fewer than the method. PerformOverloadResolution prepends the target itself when extension methods are allowed, so handing it the receiver's parameter as well left it with one argument too many: every candidate came back TooManyPositionalArguments, the reference was reported ambiguous, and the escalation ran to the end and wrote out type arguments that inference could have supplied. Two fixtures pinned that output. Both spellings compile to byte-identical IL - Use2(ints.Select) binds Enumerable.Select<int32, int32> and the same Use2 overload as Use2(ints.Select<int, int>), and .Any is the same Enumerable.Any<int32> as .Any<int> inside the expression tree - so the shorter one is what the escalation is supposed to arrive at. Assisted-by: Claude:claude-opus-5:Claude Code
Extension call syntax had no fixture of its own; it was covered only incidentally by QualifierTests, Issue1080, RefLocalsAndReturns and a few others. The cases that decide how the call is spelled - null receiver, ref/in receiver, a named argument after the receiver, type arguments inference cannot reach, params expansion, and an instance method that beats the extension so the call has to stay static - now have one place that fails if any of them moves. Assisted-by: Claude:claude-opus-5:Claude Code
Every other decision about how a member reference is spelled is made while the expression is built, behind Disambiguator, and CallBuilder already writes target.ExtensionMethod for delegate references and method groups. Extension calls were the last spelling decided afterwards, by an AST transform that had to rebuild a resolver from a UsingScope annotation on the tree root and re-derive every argument's resolve result from AST annotations. The check now runs against the resolver ExpressionBuilder already owns, whose using scope is the namespace superset the DecompileRun collected from the IL. That superset is wider than the imports actually printed, so it can only refuse infix syntax where the transform allowed it, never bind a call to a different overload. Two consequences had to be handled. The declaring type is no longer named anywhere once the call is infix, so IntroduceUsingDeclarations would not have imported its namespace and the output would not compile; it now imports from the invocation itself. And an extension GetEnumerator in infix form is spelled like an instance call, so the foreach matcher can no longer read a plain match as an instance method. Writing the receiver as the target means skipping an argument, which the argument list only supported where there were neither names nor optional arguments: it skipped before trimming the optional tail, so a skip past an omitted argument took one argument too many, and it gave up entirely on names. Both now skip after trimming, which also settles a latent off-by-one on the collection-initializer path. Assisted-by: Claude:claude-opus-5:Claude Code
siegfriedpammer
force-pushed
the
refactor/extension-method-syntax-in-callbuilder
branch
from
September 21, 2026 14:59
3fe0234 to
9b16bf3
Compare
This branch has not been deployed
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:
Tests:
Assisted-by: OpenCode:gpt-5.5:opencode