Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Core/CodeAnalysis/Binding/Binder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 21 additions & 0 deletions src/Core/CodeAnalysis/Binding/LambdaBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ internal sealed class LambdaBinder
private readonly Func<TypeSymbol, Type> resolveClrTypeForGenericArg;
private readonly Func<FunctionSymbol> getCurrentFunction;
private readonly Action<FunctionSymbol> setCurrentFunction;
private readonly Func<ParameterSyntax, ImmutableArray<BoundAttribute>> bindParameterAttributes;
private readonly Func<ExpressionSyntax, BoundExpression> bindLambdaBodyExpression;
private readonly Func<TypeParameterListSyntax, ImmutableArray<TypeParameterSymbol>> bindTypeParameterList;

Expand Down Expand Up @@ -123,6 +124,9 @@ internal sealed class LambdaBinder
/// <see cref="BindFunctionLiteralExpression"/> to push the
/// synthetic lambda function for the duration of the body bind
/// and to restore the outer function on exit.</param>
/// <param name="bindParameterAttributes">Callback that binds user
/// annotations on a lambda parameter using the declaration binder's
/// standard parameter-target validation.</param>
/// <param name="bindLambdaBodyExpression">ADR-0074 / issue #714:
/// optional callback that binds an arrow-lambda body expression.
/// Required only when <see cref="BindLambdaExpression"/> is
Expand All @@ -143,6 +147,7 @@ public LambdaBinder(
Func<TypeSymbol, Type> resolveClrTypeForGenericArg,
Func<FunctionSymbol> getCurrentFunction,
Action<FunctionSymbol> setCurrentFunction,
Func<ParameterSyntax, ImmutableArray<BoundAttribute>> bindParameterAttributes,
Func<ExpressionSyntax, BoundExpression> bindLambdaBodyExpression = null,
Func<TypeParameterListSyntax, ImmutableArray<TypeParameterSymbol>> bindTypeParameterList = null)
{
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1154,6 +1163,18 @@ public TypeSymbol WrapAsTask(TypeSymbol element, bool useValueTask = false)
return element;
}

/// <summary>Binds and attaches user annotations on a lambda parameter.</summary>
/// <param name="syntax">The source parameter syntax.</param>
/// <param name="parameter">The parameter symbol receiving the attributes.</param>
private void AttachParameterAttributes(ParameterSyntax syntax, ParameterSymbol parameter)
{
var attributes = bindParameterAttributes(syntax);
if (!attributes.IsDefaultOrEmpty)
{
parameter.SetAttributes(attributes);
}
}

/// <summary>
/// ADR-0069 amendment / issue #2442: computes the subset of the enclosing
/// scope's active narrowing frames that may legally survive into a
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// <copyright file="Issue2807AnonymousFunctionParameterAttributeEmitTests.cs" company="GSharp">
// Copyright (C) GSharp Authors. All rights reserved.
// </copyright>

using System;
using System.Diagnostics;
using System.IO;
using Xunit;

namespace GSharp.Compiler.Tests.Emit;

/// <summary>Issue #2807: anonymous functions preserve parameter attributes in emitted metadata.</summary>
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
{
}
}
}
}
Loading