65.9K
CodeProject 正在变化。 阅读更多。
Home

Slang 第二部分:CodeDOM 中的作用域和类型解析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (5投票s)

2019年12月7日

MIT

7分钟阅读

viewsIcon

4210

downloadIcon

133

从 C# 源子集中获取准确的 CodeDOM

引言

在 我们上一篇文章中,由于解析过程中缺乏类型信息,我们对 C# 的一个子集进行了相当糟糕的解析,得到了 CodeDOM。我告诉过你们,我们会跟进并更正那些未解析的节点,现在我们来了。对于刚开始阅读本文的朋友们,我们正在将 C# 源代码的一个子集转换为由 CodeDOM 表示的抽象语法树,然后可以将其渲染成任何目标语言,只要存在足够好的 CodeDOM 提供程序。由于 VB 和 C# 提供程序都随 .NET 一起提供,Slang 允许您解析 C# 子集并“开箱即用”地从中渲染 C# 或 VB 代码。还有一些针对 F# 和 TypeScript 等语言的额外提供程序,但我尚未尝试过。

再次进行此项工作的目的是创建一个库,使为您的项目添加与语言无关的代码生成功能变得非常容易。如果您的项目本身就是为其他开发人员生成代码的工具,那么这将非常有用。

关于依赖项的说明:此项目依赖于 Microsoft 的 .NET Standard 和 Core 的 CodeDOM NuGet 包。如果将此源代码转移到 .NET Framework 类库,则不需要该依赖项。不过,我这样做是为了最大程度地提高可移植性。

另外,我怎么强调都不为过,这个工具尚未准备好投入生产。它可能适用于您的项目,也可能不适用于您。它被提供作为我正在进行的这项工作的兴趣文章。我不想让大家等待一个月或更长时间,而我却在处理所有稳定性问题。

我们现在的位置

在上一篇文章中,我们处理的是一个有点简陋的概念验证解析器和从中获得的糟糕的 CodeDOM 树。解析器已经得到了一些改进,但仍然有很长的路要走。解析器本身可能会在以后更改为 ANTLR 驱动,甚至 Elk 驱动,因为错误报告很糟糕,而且我还没有足够的时间在各种源文件上对其进行测试。请关注此处。

我还添加了一个预处理器,所以现在您可以在您的 Slang 源文件中使用内置的 T4 预处理。这允许我们的代码生成具有动态性。

using System;
public class Test {
    public static void Main() {
    var j = int.MinValue;
    Console.WriteLine(10 * 2f);
<#for(var i =0;i<3;++i) {
#>        Console.WriteLine("Hello World! #<#=i+1#>");
        
<#}#>
    }
}

这将生成以下 Slang 代码

using System;
public class Test {
        public static void Main() {
        var j = int.MinValue;
        Console.WriteLine(10 * 2f);
                Console.WriteLine("Hello World! #1");

                Console.WriteLine("Hello World! #2");

                Console.WriteLine("Hello World! #3");
        }
}

您可以看到这只是(格式糟糕,但请稍候!)C# 代码。它是 C# 的一个子集,不是完整的 C#。然而,这反过来又可以生成“真正的”C# 代码

using System;

public class Test {
    public static void Main() {
        int j = int.MinValue;
        System.Console.WriteLine((10 * 2F));
        System.Console.WriteLine("Hello World! #1");
        System.Console.WriteLine("Hello World! #2");
        System.Console.WriteLine("Hello World! #3");
    }
}

或者 VB.NET 中的这个

Option Strict Off
Option Explicit On

Imports System

Public Class Test
    Public Shared Sub Main()
        Dim j As Integer = Integer.MinValue
        System.Console.WriteLine((10 * 2!))
        System.Console.WriteLine("Hello World! #1")
        System.Console.WriteLine("Hello World! #2")
        System.Console.WriteLine("Hello World! #3")
    End Sub
End Class

请注意,Option Strict Off 设置是由 Microsoft 添加的,而不是由我们的代码添加的。我们不控制输出语言的渲染。第三方 CodeDOM 提供程序负责此问题。此处,Microsoft 的 VBCodeProvider 负责此问题。

请注意,我们的类型引用是如何被完全限定的,以及我们的 var 声明是如何被转换为显式类型的变量声明。这是为了确保无论目标语言如何,都能生成明确无误的正确输出。

使用这个烂摊子

假设我们的输入模板在项目目录中并命名为 test.tt,我们可以使用以下代码获得上述结果

// Holds the output of our preprocessing:
var sw = new StringWriter();

// First preprocess our template - runs the T4 processing
// output is Slang source
using (var r = new StreamReader("..\\..\\test.tt"))
    SlangPreprocessor.Preprocess(r, sw);

// Now we parse our Slang source into our initial CodeDOM
// parse tree
var code = SlangParser.ParseCompileUnit(sw.ToString());

// We need one of these lil guys to resolve our codedom
// types and members and external types and members
var res = new CodeDomResolver();

// Give it the code we just parsed
res.CompileUnits.Add(code);

// Now we can tell Slang to fix up our tree with the type info
SlangRegenerator.Patch(res.CompileUnits);

// Finally, our tree is good. We can render it
Console.WriteLine(CodeDomUtility.ToString(code, "vb"));

我们在这里有几个步骤。第一步是预处理。我们需要运行 T4 处理输入以获取我们的 Slang。然后,我们将 Slang 解析成 CodeCompileUnit。我们还会实例化一个 CodeDomResolver,它接收我们的代码并为其添加标签,以便它可以解析作用域和类型,我们将在后面讨论。

在使用我们得到的 CodeDOM 对象之前,我们需要对其进行修补,因为从解析中得到的对象是不完整的。源代码本身不足以解析 C#。您需要类型信息。所以我们的解析只是“初始传递”,以获得基本结构。现在我们已经完成了,我们返回并应用类型信息,“纠正”我们的解析。例如,我们的解析器将 Console.WriteLine("Hello"); 视为对变量 Console 的字段 WriteLine 的委托调用。这完全不对!然而,解析器在那个时候没有足够的信息来知道更好。Patch() 通过我在此处讨论过的 CodeDOMVisitor 来处理这个问题。

最后,在修补之后,我们的 CodeDOM 树已经被折叠、修改和剪裁成一个可用的状态,所以我们只需将其传递给 CodeDomUtilityToString() 方法,并指定我们想要的语言。您可以在此处找到更多关于 CodeDomUtility 的信息:此处

如果您愿意,可以单独使用 CodeDomResolver。只需将您的 CodeCompileUnit 实例交给它,然后调用 Refresh() 即可开始使用。您可以将其用作独立系统来获取代码构造的类型和作用域信息,我们将在下面讨论。

概念化这个混乱的局面

CodeDomResolver 包含一些复杂的魔法。它基本上做的事情与编译器的中间层所做的事情类似——它在我们的代码中解析类型和作用域信息。首先,它使用 CodeDOM 对象上的 UserData 来弱引用每个元素到它的父元素。这由 Refresh() 处理。这一点很重要,这样我们就可以从任何地方回溯到树的上一级,获取我们的作用域变量、参数、成员和类型,这正是 _FillScope() 所做的。

CodeDomResolverScope _FillScope(CodeDomResolverScope result)
{
    object p;
    if(null==result.Expression)
    {
        if (null != result.TypeRef)
        {
            p = result.TypeRef;
            while (null != (p = _GetRef(p, _parentKey)))
            {
                var expr = p as CodeExpression;
                if (null != expr)
                {
                    result.Expression = expr;
                    break;
                }
            }
        }
    }
    if(null==result.Statement)
    {
        if(null!=result.Expression)
        {
            p = result.Expression;
            while(null!=(p=_GetRef(p,_parentKey)))
            {
                var stmt = p as CodeStatement;
                if (null != stmt)
                {
                    result.Statement = stmt;
                    break;
                }
            }
        } else if(null!=result.TypeRef)
        {
            p = result.TypeRef;
            while (null != (p = _GetRef(p, _parentKey)))
            {
                var stmt = p as CodeStatement;
                if (null != stmt)
                {
                    result.Statement = stmt;
                    break;
                }
            }
        }
    }
    if(null!=result.Statement)
    {
        _PopulateStatementScopeInfo(result);
    }
    if(null==result.Member)
    {
        p = null;
        if (null != result.Statement)
        {
            p = result.Statement;
        }
        else if (null != result.Expression)
            p = result.Expression;
        if(null!=p)
        { 
            while (null != (p = _GetRef(p, _parentKey)))
            {
                var mbr = p as CodeTypeMember;
                if (null != mbr)
                {
                    result.Member = mbr;
                    break;
                }
            }
        }
    }
...
}

这是一个相当长的方法,但它所做的只是从当前位置向上遍历,直到 _GetRef() 检索到的父元素,直到找到感兴趣的内容。然后它会停止并收集数据来填充相应的作用域信息,然后继续。

这使得我们可以进行一些严肃的咒语,从任何地方调用 GetScope(code) 并获取所有对我们可用的变量、参数、成员和类型。我们依靠这个来获取类型信息,以便我们可以将我们在文章开头遇到的 Console“变量”转换为它们实际上是什么——类型引用!

我们还有 GetTypeOfExpression() 等功能,可以检索(几乎)任何我们传递给它的表达式的 CodeTypeReference,以及 FillMembersOfType(),它可以获取 CodeTypeReference 对象或 Type 对象的成员。它检索声明和反射的成员,包括基类型成员。这并不容易!

我们在一个名为 _Patch() 的庞大而丑陋的方法中使用了这些信息,该方法在 SlangRegenerator 中运行 CodeDomVisitorVisit() 方法,查找任何仍然带有“slang:unresolved”的内容。目前,还有一个硬性限制,即迭代 1000 次,以便在我完成代码时,它永远不会无限期挂起,即使并非所有内容都已解析。我仍在验证我是否涵盖了这个初步交付成果中的所有元素。

其中基本上有一个匿名方法,它对各种不同的代码构造进行了一系列类型检查。这是 C# 8 的类型 switch“模式匹配”会派上用场的地方,但我在这里没有使用它。每次我们找到东西时,我们都会尝试使用我们拥有的信息来纠正它。这可能会变得很糟糕,尤其是在我们找到的字段和类型引用的情况下,但我们处理了。例如,这里是 _Patch() 中用于用显式类型填充我们的 var 声明的代码段

var vd = co as CodeVariableDeclarationStatement;
if (null != vd)
{
    if (null == vd.Type || "System.Void" == vd.Type.BaseType)
    {
        var scope = res.GetScope(vd);
        var e = res.GetTypeOfExpression(vd.InitExpression, scope);
        if (null == e || e.BaseType == "System.Void")
            more = true;
        else
        {
            vd.Type = e;
            vd.UserData.Remove("slang:unresolved");
        }
    }
}

这段代码迫切需要重构,但我希望在开始之前让它 100% 正常运行,以防我需要重新设计。

我们未来的方向

我预计我会用另一种技术替换分词器和解析器。当前版本根本没有像样的错误处理,而且我不是基于语法来工作的,而是根据我从 Microsoft 的 C# 规范中拼凑起来的内容来工作的。至少可以说,这很糟糕。我希望基于语法来工作,所以无论我使用什么,在很大程度上都取决于我最终选择的语法。即使继续使用回溯递归下降解析器也可以,只要有一个合适的语法。但是,如果我们继续使用 ANTLR 或手动实现的解析器,我们可能会转向 GLR 解析。我宁愿避免这样做,因为它可能会迫使我用 C++ 编写部分项目。

CodeDomResolver 需要重构、测试和加固,SlangRegenerator 也是如此,但基础现在已经稳定。我知道它需要如何工作,而且它基本上正在这样做,这给了我稳固的立足点来向前推进。

正如我之前提到的,这段代码对我来说仍然是一个设计和弄清楚所有这些的游乐场,所以它还没有被很好地重构,但概念是存在的。我只是想在你圣诞节之前给你一些你可以拿到手的东西。

历史

  • 2019 年 12 月 7 日 - 首次提交
© . All rights reserved.