在Roslyn的C#编译器中合成lambda表达式

5
我一直在尝试使用最近开源的Roslyn中的C#编译器进行实验,看看是否可以添加语言特性。
现在我正在尝试添加一些语法糖,一个新的前缀运算符,它基本上是一种缩写形式。目前,我正在利用已有的“&”“取地址”(在unsafe环境之外)。
我想要扩展的模式如下:&n相当于:
Property.Bind(v => n = v, () => n)

假设库中有一个名为Property.Bind的方法,其签名如下:

public static IProperty<T> Bind<T>(Action<T> set, Func<T> get)

实质上,我需要合成两个lambda:

  1. 将其参数绑定为n的lvalue,并将其参数分配给n
  2. 没有参数,评估n。然后,我需要调用Property.Bind并将这两个lambda作为参数传递。

我的以前的实验比这个容易得多,因为它们能够依赖于易于找到的现有功能,所以几乎没有什么工作要做!

但是这一次,我很难找到与我正在进行的工作类似的内容。到目前为止,我一直在逐步了解编译器如何从源代码构建BoundLambda,而这正在变得非常混乱。我正在修改Binder_Operators.cs中的BindAddressOfExpression,使其在安全上下文中开始附加一个额外的if语句:

private BoundExpression BindAddressOfExpression(PrefixUnaryExpressionSyntax node, DiagnosticBag diagnostics)
{
    if (!this.InUnsafeRegion)
    {
        BoundExpression rValue = BindValue(node.Operand, diagnostics, BindValueKind.RValue);
        BoundExpression lValue = BindValue(node.Operand, diagnostics, BindValueKind.Assignment);

        var valueParamSymbol = new SourceSimpleParameterSymbol(null, rValue.Type, 0, RefKind.None, "__v", ImmutableArray<Location>.Empty);
        var valueParam = new BoundParameter(node, valueParamSymbol);
        var assignment = new BoundAssignmentOperator(node, lValue, valueParam, RefKind.None, rValue.Type);

        var assignmentStatement = new BoundExpressionStatement(node, assignment);
        var assignmentBlock = new BoundBlock(node, ImmutableArray<LocalSymbol>.Empty, ImmutableArray.Create<BoundStatement>(assignmentStatement)) { WasCompilerGenerated = true };
        assignmentBlock = FlowAnalysisPass.AppendImplicitReturn(assignmentBlock);

所以(假设!)现在我有第一个lambda的赋值块,但是要完整地将BoundLambda包装起来似乎是一个全新的挑战。

我在想:是否有一种“作弊”的方法可以获得这种语法糖,通过请求解析器/绑定器处理C#字符串,就像它出现在实际代码的位置一样?那样,手动构建所有部分并将它们拼接在一起就不再必要了。毕竟,现有的编译器非常适合这个问题!


仅在安全区域中运行似乎会违反最小惊奇原则。无论如何,在属性访问表达式中“&p”都没有意义,那么为什么不从那里触发呢? - Ben Voigt
我只是借用 & 进行实验。如果我能让它正常工作,我计划定义一个新的运算符 % 来模仿 C++/CLI 和 C++/CX。 - Daniel Earwicker
@BenVoigt - 使用委托会使构建“BoundExpression”更容易吗? - Daniel Earwicker
我真的不知道。但是较旧的特性通常更简单。 - Ben Voigt
我不熟悉Roslyn,但是支持元编程或编译器框架的语言通常有某种准引用和拼接的概念,即允许您引用一段代码并将外部代码元素拼入其中,而不是将其全部自己编写为一系列代码元素。然而Roslyn可能还年轻,尚未支持这一点。 - Avish
显示剩余3条评论
2个回答

5

更新:我已经确定了一个名为SyntaxTemplate的新类,它是不可变的,因此可以静态创建并重复使用。例如:

private static readonly SyntaxTemplate _pointerIndirectionTemplate 
      = new SyntaxTemplate("p.Value");

private static readonly SyntaxTemplate _propertyReferenceTemplate
     = new SyntaxTemplate("System.Property.Bind(__v_pr__ => o = __v_pr__, () => o)");

private static readonly SyntaxTemplate _propertyReferenceTypeTemplate 
     = new SyntaxTemplate("System.IProperty<T>");

private static readonly SyntaxTemplate _enumerableTypeTemplate 
     = new SyntaxTemplate("System.Collections.Generic.IEnumerable<T>");

它内部有一个不可变的标识符字典,所以任何一个都可以被名称替换,例如针对以下表达式:

if (!operand.Type.IsPointerType())
    return BindExpression(
        _pointerIndirectionTemplate.Replace("p", node.Operand).Syntax, 
        diagnostics);

或者针对一种类型:
if (this.IsIndirectlyInIterator || !this.InUnsafeRegion)
    return BindNamespaceOrTypeOrAliasSymbol(
        _enumerableTypeTemplate.Replace("T", node.ElementType).Syntax,
        diagnostics, basesBeingResolved, suppressUseSiteDiagnostics);

SyntaxTemplate 的外观如下:

internal class SyntaxTemplate
{
    public ExpressionSyntax Syntax { get; private set; }

    private readonly ImmutableDictionary<string, ImmutableList<IdentifierNameSyntax>> _identifiers;

    public SyntaxTemplate(string source)
    {
        Syntax = SyntaxFactory.ParseExpression(source);

        var identifiers = ImmutableDictionary<string, ImmutableList<IdentifierNameSyntax>.Builder>.Empty.ToBuilder();

        foreach (var node in Syntax.DescendantNodes().OfType<IdentifierNameSyntax>())
        {
            ImmutableList<IdentifierNameSyntax>.Builder list;
            if (!identifiers.TryGetValue(node.Identifier.Text, out list))
                list = identifiers[node.Identifier.Text] = 
                    ImmutableList<IdentifierNameSyntax>.Empty.ToBuilder();
            list.Add(node);
        }

        _identifiers = identifiers.ToImmutableDictionary(
            p => p.Key, p => p.Value.ToImmutableList());
    }

    private SyntaxTemplate(ExpressionSyntax syntax,
        ImmutableDictionary<string, ImmutableList<IdentifierNameSyntax>> identifiers)
    {
        Syntax = syntax;
        _identifiers = identifiers;
    }

    public SyntaxTemplate Replace(string identifier, SyntaxNode value)
    {
        return new SyntaxTemplate(
            Syntax.ReplaceNodes(_identifiers[identifier], (o1, o2) => value),
            _identifiers.Remove(identifier));
    }
}

由于替换值是 SyntaxNode,因此您可以使用由解析器已创建的节点,这样就不需要浪费精力对相同语法进行两次重新解析。
更多内容:这个方法有些作用,但是如果用户的源代码中存在错误(例如,在不合适的情况下使用新语法),则绑定期间生成的错误将引用模板源代码中的位置,在用户源代码中是没有意义的。因此,IDE 不能显示红色波浪线等。
为了解决这个问题,您可以使用一个帮助方法,它将诊断信息捕获到临时包中,然后将其重放到真正的包中,并将位置更改为在用户源代码中使用您的语法的地方。
private T RedirectDiagnostics<T>(DiagnosticBag diagnostics, CSharpSyntaxNode nodeWithLocation, Func<DiagnosticBag, T> generate)
{
    var captured = new DiagnosticBag();
    var result = generate(captured);

    foreach (var diag in captured.AsEnumerable().OfType<DiagnosticWithInfo>())
        diagnostics.Add(new CSDiagnostic(diag.Info, nodeWithLocation.Location));

    return result;
}

以下是示例用法,仅包装上述第一个示例:

if (!operand.Type.IsPointerType())
    return RedirectDiagnostics(diagnostics, node, redirected => 
        BindExpression(_pointerIndirectionTemplate.Replace("p", node.Operand).Syntax, redirected));

现在红色的波浪线可以正常工作了(在真正的编译中,错误信息上的行号也是正确的)。

我至少会在替换周围添加一些括号,并可能放置一个占位符,以便在生成的AST中进行替换。 - Ben Voigt
@BenVoigt,是的,我正在考虑在语言中定义特殊的语法用于占位符。我可以使用它来替代 {0},这样就可以避免对操作数进行双重解析。甚至可以将其公开为元编程功能,使用户可以在自己的源代码中声明此类扩展。什么情况下需要括号? - Daniel Earwicker
如果将 v => {0} = v 视为文本,则 lambda 运算符 => 和赋值运算符 = 的任何优先级都将与出现在 {0} 中的任何运算符进行比较。例如,x |= y = v 将被解析为 x |= (y = v) - Ben Voigt
嗯,我曾经有一个理论,即使用 & 的用户需要自己添加括号以处理复杂情况,但如果这不是真的,那么你是正确的。但希望如果我能让占位符替代方法正常工作,那就可以完全避免这个问题。 - Daniel Earwicker
好吧,我不想打赌括号是否会被解析过程和Operand.GetText()保留。 - Ben Voigt

3

我建议你看一下查询表达式如何通过编译器生成的 lambda 表达式转换为方法调用。


+1 不确定为什么你被踩了,所以我来平衡一下。这是一个非常好的建议,我在懒惰之前就已经开始这样的调查路径了。 - Daniel Earwicker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接