Zig,Parser组合者 - 为什么他们

2021-03-11 06:10:58

在本文中,我们将探索解析器组合器是什么,运行时解析器生成是 - 为什么它们有用,然后通过Zig实现。

解析器组合器是一个高阶函数,它将解析器作为输入,并生成一个新的解析器作为输出:

假设我们想解析描述正则表达式的语法:[bc]。* abc

我们可以定义一些解析器,以帮助我们解析此语法(例如,进入令牌或AST节点):

既然我们有这些解析器,我们可以定义解析器组合器,以帮助我们解析完整的正则表达式。首先,我们需要一些东西来解析我们可以定义的字符串abc,如下所示:

它将单个解析器作为输入(在这种情况下,正则表达式拨款器)并使用它来解析输入或多次的输入。如果成功一次,则解析器组合器成功。否则,它无法解析任何内容。

现在,如果我们想解析我们正则表达式的[BC]部分,让我们说它只能包含像BC这样的文字(当然,真正的正则表达式允许远远超过这个)。重用我们的新regexstringLiteralParser:

在这种情况下,序列是一个解析器组合器,它采用多个解析器,并尝试以替换的方式解析它们,以便否则要求所有人成功或失败。

构建在此基本概念上,我们可以使用Parser Combinator构建完整的Regex语法解析器:

从之前,我们的解析器组合器RegexSyntaxParser由多个解析器(Regex ...解析器)构建,并最终生成描述给定Regex的语法的AST。

我们可以在此处使用相同的组合原则来介绍一个名为Regexparser的新的解析器生成器,它使用RegexsyntaxParser在运行时创建一个全新的解析器,该运行时能够解析实际语义的Regex描述 - 形成完整的Regex引擎:

流行的regex发动机是使用DFA(确定性有限自动机)或NFA(非匹配有限自动机)实现的,这在Russ Cox的文章中详细描述或使用Henry Spencer的虚拟机方法描述,也在Russ Cox的网站上详细描述。

值得注意的是,运行时的组合解析/生成解析器是实现正则表达引擎的罕见方法。这有点接近康复在实践中做的事情,尽管我们使用运行时解析器生成器而不是解析器解析器组合器。

人们可以争辩说我们不严格地定期解析,尽管作为Larry Wall(Perl编程语言的作者)写道,您可能习惯了现代的“regexp”模式匹配者

“正则表达式”只与真正的正则表达式略微相关。尽管如此,该术语已经增长了我们模式匹配引擎的能力,因此我不会试图在这里打击语言必需品。然而,我通常会称他们为“正面表达”(或“regexen”,当我处于盎格鲁撒克逊的心情时)。

解析器组合者倾向于以高级别语言编写的,诸如Haskell和OCAML之类的更加频繁的类型系统,这对Parser组合器等高阶函数提供了很好的函数。

我们将在Zig实现这一目标,这是一种新的低级语言,旨在成为更好的C.

ZIG具有非常酷的编译时代码执行语义,有助于提供其泛型。我们将探索这些一点,但由于我们希望在运行时最终构建解析器生成器(为了执行Regexp),我们将要查看的是主要是运行时解析器接口而不是编译时解析器接口(这是非常可能!)

由于我们将处理堆分配,因此我们的解析器将无法在现在的编辑时运行。一旦Zig获取编辑堆分配,这应该是可能的,并开启有趣的新机会。

这里是 - 在这里解压缩很多,所以我们将逐步地走过它:

PUB FN解析器(编辑值:类型,编辑读取器:类型)类型{return struct {const self = @this(); _解析:fn(self:* self,分配器:*分配器,src:* reader)callconv(内联)错误!?价值,酒吧FN解析(Self:* Self,Allocator:*分配器,SRC:*阅读器)CallConv(内联)错误!?价值{返回自我。 _parse(自我,分配器,src); }}; }

PUB FN解析器(编辑值:类型,编辑读取器:类型)类型{return struct {...};}

这是一个曲折函数,它在编号,命名值和读取器处具有两个任意类型参数。大写用于表示Zig中类型的名称。这是:

值将是解析器将生成的实际值的类型(例如,匹配文本的一串串或AST注释)

读者将成为解析的原始文本的实际源的类型(我们将稍后再介绍这一点。)

我们在这里看到的是Zig接近通用数据结构的关键方法:您只是通过类型的类型作为参数 - 好像它们是值 - 并且您将函数符合参数和返回类型作为值。对此功能有效调用的一些示例是:

解析器(U8,[] U8),其中U8是无符号的8位整数,[] U8是一片无符号的8位整数。

解析器([] const u8,@typeof(reader))其中[] const u8描述了一个utf-8文本(字符串)和reader是一些读者类型,例如std.io.fixedbufferstream(" Foobar")。

现在,由于我们试图定义一个界面,其实际实现可以在运行时交换,我们需要的是很简单:

基本上,如果有人想要实现我们的界面,他们只需要创建一个新的解析器实例并填充字段(回调),以便在使用接口时调用它们的实现。

在我们的情况下,返回的结构具有一种方法,即界面的消费者将调用调用解析 - 以及实现者将设置为返回的函数指针字段是_parse字段:

错误!?值只是描述函数可以返回错误或没有值或值类型。请参阅Zig的错误联合类型和可选类型。

CallConv(.inline)只是告诉编译器贯穿函数调用 - 因为我们的功能不是做吨。

错误{...}描述了一组潜在的错误和|| std.mem.allocator.Error只是说与我们的分配器类型的错误集合 - 所以我们的潜在错误集包括我们的错误。

当我们开始在解析器中执行不同的操作时,描述更多潜在的错误来源将变得更加复杂:

要实现解析器的所有我们需要做的就是提供_parse方法,并定义其返回值类型和读取器输入类型:

在上文中,Const解析器中的T型T型是表示命名解析器的常量类型 - 在这种情况下,它将是解析器([] U8,@typeof(Reader)返回的类型。还有这个:

是填充结构的Zig语法。我们正在将_parse字段设置为myparse。 Zig可以推断如果您编写a。{}而不是t {} - 这避免了我们对verbose的parser()函数重复调用的需要。

截至目前,我们刚刚谈论读者是任何类型的。

类似于我们的PARSER接口,ZIG标准库提供了STD.IO.READER界面,并且有许多实现者包括:

但是,与我们的解析器类型相比,它在运行时调用函数指针,std.io.reader界面是一个编译时类型 - 含义对底层实现的调用不涉及指针解密。

如今,Zig在早期阶段(版本0.7),没有像界面或特质类型的任何东西(虽然似乎这可能会在未来改进。)

这意味着,目前,我们不能简单地将我们的功能定义为仅接受STD.IO.Reader界面 - 而是必须声明我们接受我们将呼叫阅读器的任何类型,写入我们的代码,就像它是STD一样。 io.reader - 如果有人在那不是std.io.reader中,那么编译器就会只是Barf。这有时会导致混淆编译器错误消息(“标准库代码中存在错误?啊,不,我只需要通过.reader()!”)。

如果我们希望解析一个特定字符串文字的解析器接口实现,那么一种方法是还要制作接受任何读者类型的通用函数(所以我们不限于例如,只是文件输入):

这非常好 - 但我们需要某种方式来拥有我们返回我们定义的解析器界面的类型。这样做的方法是在我们的结构中定义一个字段:

PUB FN文字(编辑读取器:类型)类型{return struct {parser:parser([] u8,reader)=。 {。 _parse = parse,},}; }

如果我们希望我们的文字解析器接受一个参数 - 要查找的文字字符串 - 我们需要给它一个参数。

在仅仅将其传递一个字符串的情况下,我们可以调整签名,以便这是可能的:

但是,我们将使用Zig数据结构中更常见的Init方法来定义我们的方法:

PUB FN文字(编辑读取器:类型)类型{return struct {parser:parser([] u8,reader)=。 {。 _parse = parse,},想要:[] const u8,const self = @this(); //只要使用解析器,“想要的字符串必须保持活力。 PUB FN init(想要:[] const u8)self {return self {。想要=想要}; }}; }

在这种情况下,想要是我们想要匹配的字符串文字 - 并且[] const u8是zig的字符串类型。它描述了一片不变的(不可修改的)编码的UTF-8字节。

与c,[] const U8不同,切片意味着它是对存储器中的字符串的指针及其长度 - 所以我们不必单独地传递长度参数或使用空终止的字符串。在Zig中,有两种方法可以表示字符串:

您可以考虑这两个类似于RUST的字符串特征和不可变的& str如果[1]您[2]如[3],但RURR的字符串实际上是一个棕色的字节矢量(井,技术上是一个编译时界面可能是一个棕色的传染媒介,这些字节是在Zig中被称为std.arraylist(U8)。

我们终于准备好实际上有文字解析器解析了一些东西!我们只需要实施我们的解析方法:

PUB FN文字(编辑读取器:类型)类型{return struct {parser:parser([] u8,reader)=。 {。 _parse = parse,},想要:[] const u8,... const self = @this(); FN解析(解析器:*解析器([] U8,读者),分配器:*分配器,SRC:*阅读器)Callconv(内联)错误!? [] U8 {const self = @fieldparentptr(self,"解析器",解析器); ...}}; }

但等一下!为了使._parse =解析,分配要解决解析的第一个参数,需要是解析器的自我参数([] U8,读者) - 那么我们的解析方法如何访问我们结构的WANT Field ?

这是一些Zig魔法进入的地方:在模糊的内置函数中,我们可以在我们的解析方法内使用:

要了解这一点,请首先让我们来看看这些参数指的是什么:

自我是我们试图获取参考(我们的类型)的“父结构”

希望您可以在此处开始查看链接:解析器是指向我们的struct字段的指针,所以Zig有一个小帮手@FieldParentPtr,可以依赖于此事实给我们的结构给我们一个指向我们的结构字段的指针。

//如果返回一个值,它取决于呼叫者免费it.fn解析(解析器:*解析器([] U8,读者),分配器:*分配器,SRC:*阅读器)CallConv(.inline)错误! ?[] u8 {const self = @fieldparentptr(self,"解析器",解析器); const buf = try allocator.alloc(u8,self.want.len); const read = try src.reader()。Readall(Buf); if(读取< self.want.len或!std.mem.eql(u8,buf,self.want)){try src.seekablestream()。Seekby([电子邮件受保护](i64,读取));分配器。免费(BUF);返回null; }返回buf;}

我们正在尝试从我们的解析功能中返回一个字符串,即它发出的值是字符串(而不是AST节点)。

我们在init方法中获取的想要字符串被同意只有在解析时才有效。我们决定创建一个合同,所有的解析器实现都不会被别人给出的内存 - 或者如果他们这样做,直到解析返回直到另一种方式。因此,我们需要在我们的方法中分配一个新字符串。

通常,我们可以使用Defer(“函数结束时”)或errdefer(“如果返回错误”),但由于我们选择保留None的无选项为“我们没有解析任何”,我们需要手动自由。尼鲁莱德和索德德呢可能会很好,也许是好的吗?

为了演示如何实现解析器组合器,我们将尝试实现Oneof运算符。它将需要多次解析器作为输入,并连续运行,直到一个成功或没有。

PUB FN Oneof(编辑值:类型,编辑读取器:类型)类型{return struct {parser:解析器(值,reader)=。 {。 _parse = parse,},...}; }

您会注意到这里,与早期的文字解析器功能相比,此函数采用第二个编辑值:键入参数。这是因为我们希望它与任何现有的解析器实现一起工作,无论它产生什么类型的值。

PUB FN Oneof(编辑值:类型,编辑读取器:类型)类型{return struct {parser:解析器(值,reader)=。 {。 _parse = parse,},解析器:[] *解析器(值,阅读器),const self = @this(); //“解析器”切片必须保持活力,只要解析器将被使用。 pub fn init(解析器:[] *解析器(值,读者))self {return self {。 parsers =解析器,}; }}; }

正如您可以在这里看到的,我们只需将指针列表到解析器。他们所有人都需要具有与Oneof调用中指定的相同的返回值。

其中一个原因是Zig不支持返回类型推断。您可以拥有一个函数,它将随处为参数,但它无法返回任何类型。这只是意味着我们需要具有通用函数(在这种情况下,oneof),它接受类型参数,然后稍后使用该值类型。用像haskell或Ocaml这样的语言,这不是真的。

PUB FN Oneof(编辑值:类型,编辑读取器:类型)类型{return struct {... //呼叫者负责释放值,如果有的话。 FN解析(解析器:*解析器(价值,阅读器),分配器:*分配器,SRC:*阅读器)CallConv(内联)错误!?值{const self = @fieldparentptr(self,"解析器",解析器);对于(自我。解析器)| One_of_Parser | {const结果=尝试one_of_parser。解析(分配器,SRC); if(结果!= null){返回结果; }返回null; }}; }

尝试one_of_parser.parse(分配器,src);表示如果使用One_Of_Parser解析返回错误,则函数应立即返回,而不会继续尝试与其他解析器解析。

if(结果!= null){是你如何检查Zig中的可选类型是“无”。我发现这个非常有趣:它不是null,它实际上是一个可选的“无”类型 - 但它被称为null。我不确定为什么,但是可以想象这使得这种语言更友好的人不熟悉可选类型。

现在为COOL部分:我们可以将我们的文字解析器和Oneof Parser Combinator放在一个新的解析器!

我们经常通过@typeof(读者),这使得代码比需要更加密码,并且可以引入一个oneofliteral帮助程序,这使得上面读取:

在这里解压缩的一件事是将数组传递给init:&。{...}:

由于我们的列表在编译时已知,因此我们不必为切片分配或释放内存。如果我们的列表是动态的,我们需要这样做。

您可能想知道我们如何从文字解析器和一个解析器组合器到实际在运行时生成解析器,该运行时可以解析Regexp字符串中定义的语义。

由于我们的PARSER接口是运行时接口(您可以在运行时交换实现),并且由于我们的解析器组合器Oneof使用该接口操作(必须在编译时只知道返回值,因此它可能是通用AST节点)我们可以在运行时轻松地在运行时动态创建[] *解析器(...)的切片,这是一个Parser Combinator的结果,我们已经建造出我们的“狗,猫,绵羊”解析器。

写作Parsers,如我们的文字解析器,可以解析我们的Regexp A [BC]的组件。* ABC:

写一个像我们的Oneof Parser那样的解析器组合器,除了它解析了一系列解析器。

使用我们的序列解析器组合器和regexliteralparser构建RegexStringLiteralParser - 类似于我们如何构建“狗,猫,绵羊”解析器。

写一种名为Runtime Parser生成器的新功能名为Regexparser的RundeAxParser,它将超级熟悉:占据称为RegexSyntaxParser的解析器组合器,可以将Regexp语法转换为某个中介。

让您的功能使用Parser Combinator,如Oneof,Sequence等,以基于该中介AST在运行时构建一个全新的解析器。

我很抱歉没有给你一个完整的(甚至是部分)regex引擎:)我仍在探索这个,这是一个大型企业,如果包括在内,这篇博客文章将太长。 您可以在此处找到具有解析器和解析器组合器的最终代码的副本。 只是Zig init-exe并将它们普及到您的SRC /目录中。 如果有任何不清楚或令人困惑,我很乐意帮助:拍我电子邮件[电子邮件受保护]或留下评论黑客新闻/ Reddit,我将跟进。