为单个字符创建命名标记的优点和缺点

Advantages and disadvantages of creating named tokens for single characters

假设我有一个简单的语法,您可以使用 = 符号将数字分配给标识符。

我可以用两种方式编写解析器。 我可以直接在规则中包含字符标记 =,或者我可以为它创建一个命名标记并使用词法分析器来识别它。

选项 #1:

// lexer
[A-Za-z_][A-Za-z_0-9]* { return IDENTIFIER; }
[0-9]+ { return NUMBER; }

// parser
%token IDENTIFIER NUMBER
%%
assignment : IDENTIFIER '=' NUMBER ;

选项 #2:

// lexer
[A-Za-z_][A-Za-z_0-9]* { return IDENTIFIER; }
[0-9]+ { return NUMBER; }
= { return EQUAL_SIGN; }

// parser
%token IDENTIFIER NUMBER EQUAL_SIGN
%%
assignment : IDENTIFIER EQUAL_SIGN NUMBER ;

两种编写解析器的方法都有效,我找不到关于这种情况的良好做法的信息。 第一个片段似乎更具可读性,但这不是我最关心的问题。

这两个选项中的任何一个在其他方面更快或更有益吗?他们是出于技术原因(除了可读性之外)更喜欢一个吗?

是否有第三种更好的方法?

我问的是关于大型解析器的问题,其中优化可能是一个真正的问题,而不仅仅是这里显示的玩具示例。

除了可读性之外,基本上没有区别。确实没有优化问题,无论您的语法有多大。编译语法后,标记是小整数,一个小整数与任何其他小整数完全相同。

但我不会低估可读性的重要性。一方面,很难在不可读的代码中发现错误。简单地为某些标点符号键入错误的名称导致语法错误的情况非常普遍。与写成 T_LBRC expr T_LBRC 相比,更容易看出 '{' expr '{' 是错误的,而且对于第一语言不是英语的人来说,命名符号更难解释。

Bison 的解析 table 压缩要求令牌编号是连续的整数,这是通过查找 table 传递传入的令牌代码来完成的,这几乎不是主要的开销。但是,不使用字符代码并不能避免这种查找,因为标记编号 1 到 255 无论如何都是保留的。

但是,使用 named token constructors 的 Bison 的 C++ API 需要标记名称,并且 single-character 标记代码不能用作标记名称(尽管它们没有被禁止,因为您不是需要使用命名构造函数)。

鉴于该用例,Bison 最近引入了一个选项,该选项生成连续编号的令牌代码以避免重新编码;此选项与 single-character 标记本身不兼容。不必重新编码令牌可能是边际 speed-up,但很难相信它很重要,但如果你不打算使用 single-quoted 令牌,你也可以这样做.

就我个人而言,我不认为额外的复杂性是合理的,至少对于 C API。如果您确实选择使用标记名称,也许是为了使用 C++ API 的命名构造函数,我强烈建议您使用 Bison 别名以便使用 double-quoted 标记编写语法(也推荐用于 multi-character 个运算符和关键字标记。