From 28e793a07ba9be62e71c0ed9c36038d60cff69cb Mon Sep 17 00:00:00 2001 From: DavidObando Date: Sat, 25 Jul 2026 08:49:28 -0700 Subject: [PATCH] Preserve anonymous parameter attributes --- src/Core/CodeAnalysis/Binding/Binder.cs | 6 + src/Core/CodeAnalysis/Binding/LambdaBinder.cs | 21 ++++ ...mousFunctionParameterAttributeEmitTests.cs | 116 ++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 test/Compiler.Tests/Emit/Issue2807AnonymousFunctionParameterAttributeEmitTests.cs diff --git a/src/Core/CodeAnalysis/Binding/Binder.cs b/src/Core/CodeAnalysis/Binding/Binder.cs index 2ac9b198..7747e984 100644 --- a/src/Core/CodeAnalysis/Binding/Binder.cs +++ b/src/Core/CodeAnalysis/Binding/Binder.cs @@ -259,6 +259,12 @@ public Binder(BoundScope parent, FunctionSymbol function) resolveClrTypeForGenericArg: ResolveClrTypeForGenericArg, getCurrentFunction: () => this.function, setCurrentFunction: fn => this.function = fn, + bindParameterAttributes: syntax => declarations.BindAttributes( + syntax.Annotations, + AttributeTargetKind.Param, + ParameterAllowedTargets, + "a parameter declaration", + System.AttributeTargets.Parameter), bindLambdaBodyExpression: syntax => expressions.BindLambdaBodyExpression(syntax), bindTypeParameterList: syntax => declarations.BindTypeParameterList(syntax)); statements = new StatementBinder( diff --git a/src/Core/CodeAnalysis/Binding/LambdaBinder.cs b/src/Core/CodeAnalysis/Binding/LambdaBinder.cs index f77197c3..557f247a 100644 --- a/src/Core/CodeAnalysis/Binding/LambdaBinder.cs +++ b/src/Core/CodeAnalysis/Binding/LambdaBinder.cs @@ -75,6 +75,7 @@ internal sealed class LambdaBinder private readonly Func resolveClrTypeForGenericArg; private readonly Func getCurrentFunction; private readonly Action setCurrentFunction; + private readonly Func> bindParameterAttributes; private readonly Func bindLambdaBodyExpression; private readonly Func> bindTypeParameterList; @@ -123,6 +124,9 @@ internal sealed class LambdaBinder /// to push the /// synthetic lambda function for the duration of the body bind /// and to restore the outer function on exit. + /// Callback that binds user + /// annotations on a lambda parameter using the declaration binder's + /// standard parameter-target validation. /// ADR-0074 / issue #714: /// optional callback that binds an arrow-lambda body expression. /// Required only when is @@ -143,6 +147,7 @@ public LambdaBinder( Func resolveClrTypeForGenericArg, Func getCurrentFunction, Action setCurrentFunction, + Func> bindParameterAttributes, Func bindLambdaBodyExpression = null, Func> bindTypeParameterList = null) { @@ -155,6 +160,7 @@ public LambdaBinder( this.resolveClrTypeForGenericArg = resolveClrTypeForGenericArg ?? throw new ArgumentNullException(nameof(resolveClrTypeForGenericArg)); this.getCurrentFunction = getCurrentFunction ?? throw new ArgumentNullException(nameof(getCurrentFunction)); this.setCurrentFunction = setCurrentFunction ?? throw new ArgumentNullException(nameof(setCurrentFunction)); + this.bindParameterAttributes = bindParameterAttributes ?? throw new ArgumentNullException(nameof(bindParameterAttributes)); this.bindLambdaBodyExpression = bindLambdaBodyExpression; this.bindTypeParameterList = bindTypeParameterList; } @@ -266,6 +272,7 @@ public BoundExpression BindFunctionLiteralExpression(FunctionLiteralExpressionSy // default value; lambdas can be invoked through their delegate type // which honors the default at the call site. conversions.BindAndAttachParameterDefaultValue(p, lambdaParam); + AttachParameterAttributes(p, lambdaParam); parameterSymbols.Add(lambdaParam); parameterTypes.Add(ptype); } @@ -678,6 +685,7 @@ public BoundExpression BindLambdaExpression(LambdaExpressionSyntax syntax, Funct // default at bind time the same way it does for function-literal // parameters. conversions.BindAndAttachParameterDefaultValue(p, lambdaParam); + AttachParameterAttributes(p, lambdaParam); parameterSymbols.Add(lambdaParam); parameterTypes.Add(ptype); } @@ -885,6 +893,7 @@ public BoundFunctionLiteralExpression CreateErasedFunctionLiteralAdapter( declaringSyntax: original.DeclaringSyntax, isScoped: original.IsScoped, refKind: original.RefKind); + adapterParameter.SetAttributes(original.Attributes); adapterParameters.Add(adapterParameter); replacementMap[original] = new BoundConversionExpression( null, @@ -1154,6 +1163,18 @@ public TypeSymbol WrapAsTask(TypeSymbol element, bool useValueTask = false) return element; } + /// Binds and attaches user annotations on a lambda parameter. + /// The source parameter syntax. + /// The parameter symbol receiving the attributes. + private void AttachParameterAttributes(ParameterSyntax syntax, ParameterSymbol parameter) + { + var attributes = bindParameterAttributes(syntax); + if (!attributes.IsDefaultOrEmpty) + { + parameter.SetAttributes(attributes); + } + } + /// /// ADR-0069 amendment / issue #2442: computes the subset of the enclosing /// scope's active narrowing frames that may legally survive into a diff --git a/test/Compiler.Tests/Emit/Issue2807AnonymousFunctionParameterAttributeEmitTests.cs b/test/Compiler.Tests/Emit/Issue2807AnonymousFunctionParameterAttributeEmitTests.cs new file mode 100644 index 00000000..3aff969a --- /dev/null +++ b/test/Compiler.Tests/Emit/Issue2807AnonymousFunctionParameterAttributeEmitTests.cs @@ -0,0 +1,116 @@ +// +// Copyright (C) GSharp Authors. All rights reserved. +// + +using System; +using System.Diagnostics; +using System.IO; +using Xunit; + +namespace GSharp.Compiler.Tests.Emit; + +/// Issue #2807: anonymous functions preserve parameter attributes in emitted metadata. +public class Issue2807AnonymousFunctionParameterAttributeEmitTests +{ + [Fact] + public void AnonymousFunctionParameters_PreserveAttributes() + { + var source = """ + package P + + import System + + class MarkerAttribute : Attribute { + } + + func attributeCount(handler Func[int32, int32]) int32 { + return handler.Method.GetParameters()[0].GetCustomAttributes(false).Length + } + + func stringAttributeCount(handler Func[string, string?]) int32 { + return handler.Method.GetParameters()[0].GetCustomAttributes(false).Length + } + + let functionLiteral Func[int32, int32] = func(@Marker value int32) int32 { + return value + 1 + } + let arrowLambda Func[int32, int32] = (@Marker value int32) -> value + 1 + let widenedAdapter Func[string, string?] = func(@Marker value string) string { + return value + } + + Console.WriteLine(attributeCount(functionLiteral)) + Console.WriteLine(attributeCount(arrowLambda)) + Console.WriteLine(stringAttributeCount(widenedAdapter)) + """; + + Assert.Equal("1\n1\n1\n", CompileAndRun(source)); + } + + private static string CompileAndRun(string source) + { + var tempDir = Directory.CreateTempSubdirectory("gs_issue2807_").FullName; + try + { + var srcPath = Path.Combine(tempDir, "test.gs"); + var outPath = Path.Combine(tempDir, "test.dll"); + File.WriteAllText(srcPath, source); + + using var compileOut = new StringWriter(); + using var compileErr = new StringWriter(); + var previousOut = Console.Out; + var previousErr = Console.Error; + Console.SetOut(compileOut); + Console.SetError(compileErr); + int compileExit; + try + { + compileExit = Program.Main(new[] + { + "/out:" + outPath, + "/target:exe", + "/targetframework:net10.0", + srcPath, + }); + } + finally + { + Console.SetOut(previousOut); + Console.SetError(previousErr); + } + + Assert.True(compileExit == 0, $"compile failed ({compileExit}): {compileOut}{compileErr}"); + IlVerifier.Verify(outPath); + + var processInfo = new ProcessStartInfo("dotnet") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + WorkingDirectory = tempDir, + }; + processInfo.ArgumentList.Add("exec"); + processInfo.ArgumentList.Add("--runtimeconfig"); + processInfo.ArgumentList.Add(Path.ChangeExtension(outPath, ".runtimeconfig.json")); + processInfo.ArgumentList.Add(outPath); + + using var process = Process.Start(processInfo); + var stdout = process!.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + Assert.True(process.WaitForExit(30_000), "dotnet exec timed out"); + Assert.True(process.ExitCode == 0, $"exited {process.ExitCode}\nstdout:\n{stdout}\nstderr:\n{stderr}"); + + return stdout.Replace("\r\n", "\n"); + } + finally + { + try + { + Directory.Delete(tempDir, recursive: true); + } + catch + { + } + } + } +}