Skip to content

Fix #889: support arrow lambdas as delegate (System.Action/Func) arguments#895

Merged
DavidObando merged 1 commit into
mainfrom
fix/issue-889-arrow-lambda-action
Jun 19, 2026
Merged

Fix #889: support arrow lambdas as delegate (System.Action/Func) arguments#895
DavidObando merged 1 commit into
mainfrom
fix/issue-889-arrow-lambda-action

Conversation

@DavidObando

Copy link
Copy Markdown
Owner

Fixes #889

Root cause

Arrow lambdas (() -> expr) could not be passed where a void-returning delegate such as System.Action was expected, while the func() { ... } statement-body form worked.

This is a natural-typing gap. An arrow lambda whose trailing expression yields a value — e.g. () -> called = called + 1 (an assignment evaluates to int32) — infers the natural type () -> int32, which is not convertible to () -> void (System.Action). The func() { ... } form has a statement body, so it naturally returns void. This matches C#, where var f = () => x++; is a Func<int>; the fix is therefore target-typing the literal to the delegate parameter, not changing its natural type.

Fix

Void-izes a func/arrow literal to a void-returning delegate by discarding its trailing value — mirroring the existing func statement-body behaviour and C#'s lambda-to-Action conversion:

  • LambdaBinder — when the target delegate returns void, pin the inferred return type to void; the erased-adapter rewriter now drops the trailing value (return <v>;<v>; return;), producing valid void IL.
  • ConversionClassifier — void-ize function literals on direct BindConversion paths (covers let a Action = () -> ...).
  • OverloadResolver — void-ize literal arguments at user function / constructor / instance call sites before the wrong-argument-type gate.
  • OverloadResolution — add a lowest-priority LambdaToVoidDelegate conversion kind so that a value-returning Func overload still wins betterness, while enabling imported-CLR method resolution (e.g. CliEnvironment.RegisterRestore(Action)).
  • ExpressionBinder.Calls — void-ize literal args on the static imported-call path, mirroring the instance path.
  • MemberLookup — add TryGetDelegateFunctionTypeFromSymbol to resolve the function-type shape of native function types, named delegates, and imported CLR delegates.

Void-izing is restricted to function literals, so natural typing for function-typed values is preserved and existing overload-resolution behaviour is unchanged (e.g. Task.Run(() -> ...) still prefers the Func overload).

Tests

New test/Compiler.Tests/Emit/Issue889ArrowLambdaActionEmitTests.cs (CompileAndRun + ilverify) covering:

  • arrow lambda → System.Action variable (assignment and +=),
  • arrow lambda passed to a user function expecting Action / func(int32),
  • value-returning func() int32 keeps natural typing,
  • arrow lambda passed to an imported static method taking System.Action (mirrors the issue; helper assembly emitted via PersistedAssemblyBuilder).

Local results

  • dotnet build GSharp.sln -c Release --no-restore -graphBuild succeeded (0 warnings, 0 errors).
  • dotnet test GSharp.sln -c Release --no-buildall green: Sdk 38, Extensions 104, Interpreter 308, LanguageServer 326, Core 3340 (1 skipped), Compiler 1347. 0 failures.

…ments

Arrow lambdas (`() -> expr`) could not be passed where a void-returning
delegate such as System.Action was expected, while the `func() { ... }`
statement-body form worked. The root cause is natural typing: an arrow
lambda whose trailing expression yields a value (e.g. `() -> called =
called + 1`) infers a value return type (`() -> int32`), which is not
convertible to `() -> void` (System.Action). The `func` statement-body
form naturally yields void.

The fix target-types/void-izes a func/arrow *literal* to a void-returning
delegate by discarding its trailing value, mirroring C#'s behaviour and
the existing func-statement-body form:

- LambdaBinder: pin inferred return type to void when the target delegate
  returns void; the erased adapter rewriter now drops the trailing value
  (`return <v>;` -> `<v>; return;`) producing valid void IL.
- ConversionClassifier: void-ize function literals on direct BindConversion
  paths (covers `let a Action = () -> ...`).
- OverloadResolver: void-ize literal arguments at user function/constructor/
  instance call sites before the wrong-argument-type gate.
- OverloadResolution: add a lowest-priority LambdaToVoidDelegate conversion
  kind so a value-returning Func overload still wins betterness, enabling
  imported-CLR method resolution (e.g. RegisterRestore(Action)).
- ExpressionBinder.Calls: void-ize literal args on the static imported-call
  path, mirroring the instance path.

Restricting void-izing to function *literals* preserves natural typing for
function-typed values and keeps existing overload-resolution behaviour
(e.g. Task.Run(() -> ...) still prefers the Func overload).

Adds Issue889ArrowLambdaActionEmitTests covering variable assignment,
compound assignment, user-function Action/Action<int> args, value-returning
Func natural typing, and an imported static method taking System.Action.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@DavidObando DavidObando merged commit f6714fa into main Jun 19, 2026
7 checks passed
@DavidObando DavidObando deleted the fix/issue-889-arrow-lambda-action branch June 19, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot pass lambda as parameter to function that receives System.Action

1 participant