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






4.20/5 (5投票s)
从 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
树已经被折叠、修改和剪裁成一个可用的状态,所以我们只需将其传递给 CodeDomUtility
的 ToString()
方法,并指定我们想要的语言。您可以在此处找到更多关于 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
中运行 CodeDomVisitor
的 Visit()
方法,查找任何仍然带有“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 日 - 首次提交