Skip to content

Implement direct delegates#19993

Open
kerams wants to merge 10 commits into
dotnet:mainfrom
kerams:del
Open

Implement direct delegates#19993
kerams wants to merge 10 commits into
dotnet:mainfrom
kerams:del

Conversation

@kerams

@kerams kerams commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

Implements fsharp/fslang-suggestions#1083, fixes #11898. RFC with unresolved questions.

The IlxGen.fs diff is slightly weird, but basically lines 7465-7623 are new, and the remaining function body below is just existing code indented further to the right and otherwise unchanged.

I left a lot of the comments in place to make reviewing easier, but they can be certainly removed later. Test cases should cover everything mentioned in the language suggestion.

Considering this can be seen as a breaking change, we could also expose it behind an opt-in compiler flag, rather than language feature.

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes
  • Release notes entry updated:

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
LanguageFeatures.fsi docs/release-notes/.Language/preview.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of inner functions here. Not sure where exactly to place them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does delegate codegen have a lot of dependencies on other parts of IlxGen? Could it with all its inner functions perhaps live in a separate file? IlxGen.Delegates.fs or so?

@github-actions github-actions Bot added the ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen label Jun 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Compiler-Output
Affects-Compiler-Output: modifies IlxGen.fs IL emission for delegate expressions

Generated by PR Tooling Safety Check · opus46 4.6M ·


module DirectDelegates =

let private coreOptions compilation =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you pls also add a call to the runtime's IL verifier, to make sure we produce valid IL ?

Example here, there are more helpers for it.

|> verifyPEFileWithSystemDlls
|> withOutputContainsAllInOrderWithWildcards [
"All Classes and Methods in*SimpleTypesInNamespace.exe Verified."
]

@@ -0,0 +1,417 @@

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there are valid , by-design (or by test-case) reasons for deviations across optimize/realsig switches, it is better to have a single .bsl file covering more runs.

That way the tests hold equal codegen for both versions together, and guarantee it cannot deviate.

(the test framework allows for that - if e.g. .RealInternalSignature{b}. part is missing, it takes the common name.

@kerams kerams Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are deviations depending on language version and optimizations. Therefore we have 4 snapshots of everything and can prove the feature is on under specific circumstances only. I suppose I can remove the signature switching though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But honestly, the language feature clearly gates this, so if you think there is no point asserting the non-preview behavior, I'll happily remove half of the baselines.

Comment on lines +7476 to +7485
not cenv.options.localOptimizationsEnabled
&& tmvs |> List.exists (fun (v: Val) -> not v.IsCompilerGenerated)
then
// Eta-expanded delegate in an unoptimized build: the Invoke parameters are the user's own lambda
// variables, so keep the closure to preserve their names for debugging. Non-eta delegates (whose
// parameters are synthesized by BuildNewDelegateExpr) are always made direct; eta-expanded ones
// are made direct only in optimized builds, where debuggability is not a concern and the
// forwarding call survives to here.
None
else

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are sure we are not losing any sequence points for any forms of lambdas, maybe the new stepping test helpers from @auduchinok could be used to guard that behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, None here causes a fallback to existing behavior, completely unchanged.

Comment thread src/Compiler/CodeGen/IlxGen.fs Outdated
Comment on lines +7551 to +7557
hasWitnesses
|| newobj
|| isSuperInit
|| isSelfInit
|| isBaseCall
|| not (receiverShapeOk takesInstanceArg)
|| receiverNotBindable ()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like if this block of code has better explanation and maybe even encapsulation if possible (similar one is also for ILMethod ). To make sure future extensions of runtime/language know when to extend this block.


open System

// Section D, bullet 1: the optimizer inlines small function bodies before IlxGen runs. If a delegate

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another dimension: Inlining across F# assembly boundaries

// 3. eta-expanded known target, tupled application, same compiled representation
let case3_etaTupled () = Action<int, int>(fun a b -> handlerTupled (a, b))

// 15. non-eta-expanded known target via partial application

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make sure the numbering is unified with the RFC cases


open System

// Section D, bullet 1: the optimizer inlines small function bodies before IlxGen runs. If a delegate

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D?


open System

// Section D, bullet 2: cases 15/16 (in DelegateKnownFunction.fs / DelegateStaticMethod.fs) capture a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean the development-time pointers/indices please.

|> coreOptions
|> withLangVersionPreview
|> withPreviewBaseline
|> compile

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least a representative for each positive case should be also running, not just compile.

|> coreOptions
|> compile
|> shouldSucceed
|> verifyILBaseline

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thought - for the positive scenarios, can we be more specific and instead of .bsl do a pair of "contains" (function pointer load) and "not contains" (no closure allocation).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ldftn/ldvirtftn are present in both cases, along with putting arguments on the stack. The latter also requires a dup instruction. If your goal is to minimize potential future baseline churn, I'm not sure this would help.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the goal would to minimize the chance of an unrelated change affecting the .bsl.
But the code samples are well isolated, so its not a big deal.

@kerams

kerams commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Ready for another pass, think I addressed everything. (But alternatives and unresolved questions in the RFC need answers still:))

@smoothdeveloper

Copy link
Copy Markdown
Contributor

Add an opt-out compiler flag (default on) alongside the language feature.

I think this is not needed anymore, Tomas has added a msbuild way as well as compiler flag to disable specific language features:

--disableLanguageFeature:DirectDelegateConstruction

or

 <ItemGroup>
  <DisabledLanguageFeatures Include="DirectDelegateConstruction" />
</ItemGroup>

You may want to check it picks up as is to allow selectively disabling the feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

Don't emit unnecessary closures

3 participants