Skip to content

Consider an alternative representation for binary expressions in the syntax model #70865

Description

@CyrusNajmabadi

Background and Motivation

The IDE has found that we effectively cannot ever write a recursive algorithm that processes syntax trees (which also includes using the SyntaxWalker or SyntaxRewriter classes). This is because in realistic, non-pathological cases, we encounter trees that contain node-paths so long, that any sort of recursion blows the stack. Specifically, the node paths are all similar to the form:

T X =
    "..." +
    "..." + 
    "..." + 
    // etc
    "...";

Basically, the user has a large, repetitive expression that they either generated, or they built manually. This is very common with string expressions (where people like to break things into lines), but also occurs with other binary operators.

Unfortunately, because we chose to represent these expressions with an unbalanced tree, we end up with the following

        +
"..."    +
 "..."    +
  "..."    +
//etc
            +
      "..."   "..."

In all other places where we see long lists of constructs, we have actually used lists in the syntax model, not unbalanced trees. For example, we are never really gated on hte number of statements in a block, since that's represented as a SyntaxList<StatementSyntax> and processing that list is practically done with iteration, not recursion.

Proposed API

The proposal here is fairly drastic, but i think worth consideration. I think we should effectively stop producing BinaryExpression nodes (marking them as obsolete), and move to a new NAryExpression node like so:

+[Obsolete("No longer created")]
public sealed class BinaryExpressionSyntax : ExpressionSyntax
{
    public ExpressionSyntax Left { get; }
    public ExpressionSyntax Right { get; }
}

+public sealed class NAryExpressionSyntax : ExpressionSyntax
+{
+    public SeparatedSyntaxList<ExpressionSyntax> Expressions { get; }
+}

The change here is that instead of producing unbalanced binary trees for long expressions like the one above, we would instead then produce a linear sequence of "..." + "..." + etc. within the NAryExpressionSyntax.

We would only produce such a sequence for all the consecutive expressions using the same operator (and as such the same precedence). In other words, if you had:

a * b * c * d + e + f + g + h

That would be represented as:

                      NAryExpression (SyntaxKind.AddExpression)
                     /                                         \
                    /                                      + e + f + g + h
  NAryExpression (SyntaxKind.MultiplyExpression)               
                     |
               a * b * c * d

But this would still be much better, with only one level of nesting, instead of 8.

Risks

Risks are very large here. We'd be effectively dramatically changing the syntax we create for a core case. We have stated from the beginning that that is something we might do, but we've never done so before.

However, my feeling is htat this is worth it. Primarily because the existing model is problematic for everyone. Compiler, IDE, and all our analyzer authors end up constantly having to work around this all the time. And we continually get stack overflow issues from release to release where this is missed. Since everyone has has to deal with this anyways, and it's an ongoing tax to do so, we might as well bite the bullet and move to this safer model.

As a plus, it means that we no longer have to ban certain APIs from being used as they would now be effectively safe for real world code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Concept-APIThis issue involves adding, removing, clarification, or modification of an API.Feature Request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions