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

Visual FA 第 4 部分:使用 Visual FA C# 源生成器生成匹配器和词法分析器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2024年2月2日

MIT

4分钟阅读

viewsIcon

7412

downloadIcon

118

通过这个简单的 NuGet 包和一些注释,轻松地将词法分析器添加到您的项目中

引言

文章列表

更新:现在您也可以标记整个类。说明如下。

我撒谎了。在第 3 部分(之前的文章)中,我声称我们将涵盖 Visual FA 中源代码生成功能的代码。 事实证明,在这段时间内,我添加了一种完全不同的源代码生成机制,并且值得单独探讨。

这需要 C#9,所以您应该至少使用 .NET 6,如果需要,我将假设您正在使用 Visual Studio。

我也假设您一直在关注,并且在来到这里之前已经阅读了本系列的至少第 1 部分。 我将不会介绍 Visual FA 是什么,并且将简要介绍其作为词法分析器和匹配器生成引擎的设施。

背景

C#9 引入了在 C# 中钩住编译器进程并让它在编译二进制文件期间注入额外的动态创建代码的能力。这项技术被称为源代码生成器,它们是增强语言并增加额外功能的强大方法,即使它偶尔使用起来有些古怪。

我制作了一个 NuGet 包,一旦被您的项目引用,就可以让您使用属性标记 partial 方法,这些属性告诉编译器如何为您实现匹配器或词法分析器。生成的代码将使用 VisualFA 库(如果您的项目引用了它),否则它将根据需要将核心共享依赖代码注入到您的项目中*。

* 此源代码生成功能实际上不适用于 VisualFA.brick.cs,因为它无法在我的代码被调用的编译阶段检测到该文件在您的项目中是否存在。您要么必须引用 VisualFA.dll,要么必须完全放弃引用 Visual FA。这两个选项都可以,只是 brick 文件不行。

Using the Code

标记的核心是 FARuleAttribute

[AttributeUsage(AttributeTargets.Method,AllowMultiple = true,Inherited = false)]
class FARuleAttribute : System.Attribute
{
    public FARuleAttribute() { }
    public FARuleAttribute(string expression)
    { 
        if (expression == null) 
            throw new ArgumentNullException(nameof(expression));
        if (expression.Length == 0) 
            throw new ArgumentException(
                "The expression must not be empty", 
                nameof(expression));
        Expression = expression;
    }
    public string Expression { get; set; } = "";
    public string BlockEnd { get; set; } = null;
    public int Id { get; set; } = -1;
    public string Symbol { get; set; } = null;
}

请注意,您不会在库或源代码中找到此属性。 它在您第一次使用它后被注入到您的代码中,任何支持类型也是如此。 因此,您第一次使用这些时,您的代码中会出现红色波浪线。 只要代码正确无误,只需构建,这些波浪线就会自行解决。

在探索它的所有功能之前,我们先在代码中看一下它

[FARule(@"\/\*", Id = 0, Symbol = "commentBlock", BlockEnd = @"\*\/")]
[FARule(@"\/\/[^\n]*", Id = 1, Symbol = "commentLine")]
[FARule(@"[ \t\r\n]+", Id = 2, Symbol = "whiteSpace")]
[FARule(@"[A-Za-z_][A-Za-z0-9_]*", Id = 3, Symbol = "identifier")]
internal partial FAStringRunner MyLexer(string text);

在这里,我们已将 MyLexer() 方法定义为具有四个规则。

每个规则都有一个正则表达式 - 通常是第一个未命名的参数,然后是几个命名的可选参数:BlockEndIdSymbol。 其中每一个都代表词法分析器中的一个规则。 在这里,我们将 commentBlockcommentLinewhiteSpaceidentifier 定义为符号,并为每个规则提供了一个 ID,尽管我们不必这样做 - 如果未提供,它们将在生成过程中填充。 如果您不记得块结束的目的,请再次参阅本系列的第一篇文章,但基本上它是一个额外的表达式,用于匹配多字符结束条件。

现在进入方法签名本身。 它必须是部分类中的部分方法。 它必须要么不带任何参数,要么带一个 string 参数,要么带一个 TextReader 参数。 它必须返回 FAStringRunnerFATextReaderRunnerFAStringDfaTableRunnerFATextReaderDfaTableRunner。 也可以返回 FARunner,但前提是您的函数带有一个参数。

本质上,返回类型用于推断要生成哪种运行器,例如它应该对字符串还是文本读取器进行操作,以及它是编译的还是表格驱动的。

无论如何,一旦您创建了您的类和方法,您就可以像这样使用它

var exp = "the 10 quick brown #@%$! foxes jumped over 1.5 lazy dogs";
foreach (var match in MyClass.MyLexer(exp))
{
    Console.WriteLine(match);
}
return;

给定上述定义,什么会产生以下结果到控制台

[SymbolId: 3, Value: "the", Position: 0 (1, 1)]
[SymbolId: 2, Value: " ", Position: 3 (1, 4)]
[SymbolId: -1, Value: "10", Position: 4 (1, 5)]
[SymbolId: 2, Value: " ", Position: 6 (1, 7)]
[SymbolId: 3, Value: "quick", Position: 7 (1, 8)]
[SymbolId: 2, Value: " ", Position: 12 (1, 13)]
[SymbolId: 3, Value: "brown", Position: 13 (1, 14)]
[SymbolId: 2, Value: " ", Position: 18 (1, 19)]
[SymbolId: -1, Value: "#@%$!", Position: 19 (1, 20)]
[SymbolId: 2, Value: " ", Position: 24 (1, 25)]
[SymbolId: 3, Value: "foxes", Position: 25 (1, 26)]
[SymbolId: 2, Value: " ", Position: 30 (1, 31)]
[SymbolId: 3, Value: "jumped", Position: 31 (1, 32)]
[SymbolId: 2, Value: " ", Position: 37 (1, 38)]
[SymbolId: 3, Value: "over", Position: 38 (1, 39)]
[SymbolId: 2, Value: " ", Position: 42 (1, 43)]
[SymbolId: -1, Value: "1.5", Position: 43 (1, 44)]
[SymbolId: 2, Value: " ", Position: 46 (1, 47)]
[SymbolId: 3, Value: "lazy", Position: 47 (1, 48)]
[SymbolId: 2, Value: " ", Position: 51 (1, 52)]
[SymbolId: 3, Value: "dogs", Position: 52 (1, 53)]

另一个选项是标记一个类。 这样做的好处是您可以访问最终运行器的类型,包括其符号常量(如果使用了定义中的 Symbol

[FARule(@"\/\*", Id = 0, Symbol = "commentBlock", BlockEnd = @"\*\/")]
[FARule(@"\/\/[^\n]*", Id = 1, Symbol = "commentLine")]
[FARule(@"[ \t\r\n]+", Id = 2, Symbol = "whiteSpace")]
[FARule(@"[A-Za-z_][A-Za-z0-9_]*", Id = 3, Symbol = "identifier")]
partial class MyLexer : FAStringRunner {

}

然后您可以像这样使用它

var exp = "the 10 quick brown #@%$! foxes jumped over 1.5 lazy dogs";
var runner = new MyLexer();
runner.Set(exp);
foreach (var match in runner)
{
    Console.WriteLine(match);
}
// you can also do things like if(match.SymbolId == FooLexer.whiteSpace) ...
return;

目前就这样了。 稍后我将添加更多功能。

历史

  • 2024 年 2 月 2 日 - 初始提交
  • 2024 年 2 月 5 日 - 向生成器添加了类标记
© . All rights reserved.