区分“>>”和“>”解析泛型类型时

时间:2014-01-16 02:34:23

标签: c# parsing generics

我的第一个Stack Overflow问题。

我一直很好奇。

假设您正在解析以下代码行:

List<Nullable<int>> list = new List<Nullable<int>>();

解析时,一个天真的标记器会假设两个右尖括号是一个右移'#34;令牌。我还没有用C风格的语言来解决这个问题。

现代解析器如何处理这个问题?使用这样的&#34;贪婪解析时是否有解决方法?&#34;

我曾考虑在解析器中使用堆栈结构来处理这些令牌,特别是在解析泛型类型时。我不确定在编写代码编辑器时效果如何。

非常感谢! :)

3 个答案:

答案 0 :(得分:8)

解析语言时,通常有两个主要组件:扫描程序和解析程序。扫描程序生成一个令牌流,解析器根据grammar解释该流,这是该语言中生产规则的正式定义 - 您可以找到C#4.0 here的语法。

免责声明:我并不是说以下必然是如何解析C#语言,我只是使用C#片段来说明一般概念。

<强>扫描

所以第一步是为解析器生成标记。令牌通常由某种符号类型(表示令牌的类型),词汇(令牌的实际文本)以及其他信息(如行号)(用于错误处理)组成。

因此,如果我们使用您问题中的List<Nullable<int>> list;作为示例,则扫描程序会生成以下令牌:

available_identifier, List
<
available_identifier, Nullable
<
integral_type, int
>
>
available_identifier, list
;

请注意,令牌类型是从与上面链接的C#语法推断出来的。

<强>解析

大多数解析器都称为shift-reduce parsers。这意味着令牌会逐渐转移到堆栈上,并在符合规则时减少(删除)。为了帮助匹配,解析器将具有一定数量的可能观察到的前瞻标记(我相信一个是最常见的)。通常,当所有令牌都减少时,成功的解析将结束。

由编译器构造程序(如YACCGPPG实现的解析器类型称为LALR(1)解析器。这些工作通过构建基于解析器状态和先行符号的每个合法组合的解析表,并给出当前状态和下一个符号,然后可以告诉我们如何计算下一个状态。

现在我们有了令牌,我们将它们解压缩到解析器中,其结果通常是abstract syntax tree,它可以用于后续任务,如代码生成,语义类型检查等。解析那些令牌,我们需要规则将它们组合成有意义的句法单元 - 这就是在遇到>>时防止混淆的原因。

来自C#语法:

declaration_statement:
    | local_variable_declaration ";" 
    | local_constant_declaration ";" 

local_variable_declaration:
    | local_variable_type local_variable_declarators 

local_variable_type:
    | type 
    | "var"

local_variable_declarators:
    | local_variable_declarator 
    | local_variable_declarators "," local_variable_declarator 

local_variable_declarator:
    | identifier 
    | identifier "=" local_variable_initializer 

type:
    | value_type 
    | reference_type 
    | type_parameter 
    | type_unsafe 

value_type:
    | struct_type 
    | enum_type 

struct_type:
    | type_name 
    | simple_type 
    | nullable_type 

simple_type:
    | numeric_type 
    | bool 

numeric_type:
    | integral_type 
    | floating_point_type 
    | decimal 

integral_type:
    | "sbyte" 
    | "byte" 
    | "short" 
    | "ushort" 
    | "int"
    | "uint" 
    | "long"
    | "ulong" 
    | "char"

reference_type:
    | class_type 
    | interface_type 
    | array_type 
    | delegate_type 

class_type:
    | type_name 
    | "object"
    | "dynamic" 
    | "string"

type_name:
    | namespace_or_type_name 

namespace_or_type_name:
    | identifier type_argument_list? 
    | namespace_or_type_name "." identifier type_argument_list? 
    | qualified_alias_member 

identifier:
    | available_identifier 
    | "@" identifier_or_keyword 

type_argument_list:
    | "<" type_arguments ">" 

type_arguments:
    | type_argument 
    | type_arguments "," type_argument 

type_argument:
    | type 

看起来很复杂,但和我在一起。每条规则的格式为

rule_name:
    | production_1
    | production_2
    | production_2

每个产品可以是另一个规则(非终端)或终端。以integral_type规则为例:它的所有产品都是终端。规则也可以引用自己,这就是Tuple<int, int, double>中类型参数的处理方式。

出于本示例的目的,我假设List<Nullable<int>> list;是局部变量声明。您可以在Shift-Reduce Parsing维基百科页面上找到更简单的示例,在LR Parsing页面上找到另一个示例。

首先,我们的Parse Stack是空的,我们的单个前瞻标记是第一个,我们的第一个动作是移动该标记。也就是说,我们的解析器状态将如下所示:

Step 0
Parse Stack:    empty
Look Ahead:     available_identifier
Unscanned:      List<Nullable<int>> list;
Parser Action:  Shift

在下一步中,我们可以根据生产identifier <- available_identifier减少当前令牌。

Step 1
Parse Stack:    available_identifier
Look Ahead:     "<"
Unscanned:      <Nullable<int>> list;
Parser Action:  Reduce by identifier <- available_identifier

前面几步,在步骤10,我们将有以下解析器状态:

Step 10
Parse Stack:    identifier "<" identifier "<" type_arguments ">"
Look Ahead:     ">"
Unscanned:      > list;
Parser Action:  Reduce by type_argument_list <- "<" type_arguments ">"

此时我们将能够减少最后三个令牌,因为它们的序列组成type_argument_list(您可以在上面的规则中查看)。快进一步到第13步,我们有以下内容:

Step 13
Parse Stack:    identifier "<" type_arguments ">"
Look Ahead:     ">"
Unscanned:      list;
Parser Action:  Reduce by type_argument_list <- "<" type_arguments ">"

就像在第10步中一样,我们减少了type_argument_list <- "<" type_arguments ">"。在这样做的过程中,我们实际上避免了与>>的任何歧义。这些步骤会一直持续到我们减少declaration_statement <- local_variable_declaration ";" - 上面的第一条规则。

<强>摘要

通过创建一个明确的语法,解析器能够轻松消除像List<Nullable<int>>这样看似棘手的情况的歧义。我在这里介绍的基本上是一个自下而上的LALR(1)解析器。我还没有进入抽象语法树的实际创建,但是你可能已经有足够的内容了。

请记住,规则不包括启动状态 - 这主要是为了简洁起见。如果它有用,我可以将其余的解析步骤放在那里。

修改:f(g<a, b>(c))

在语法中归结为两个invocation_expression规则,其形式为invocation_expression -> primary_expression ( argument_list? )

第一个匹配g<a, b>(c)。它是通过首先确定g<a,b>identifier后跟type_argument_list来实现的。我们的前瞻现在是"(",因为解析器将从之前的上下文知道此代码在方法体中,它可以通过

减少identifier type_argument_list
primary_expression <- primary_no_array_creation_expression
    <- simple_name <- identifier type_argument_list?

转移"("c后,我们可以通过

减少c
argument_list <- argument <- argument_value <- expression
    <- <a really long list of rules> <- simple_name
    <- identifier <- available_identifier

转移最后的括号字符给我们

primary_expression ( argument_list? )

然后可以通过invocation_expression规则缩小,从而匹配g<a, b>(c)

到目前为止,我们已将fidentifier匹配并应用了缩减

primary_expression <- primary_no_array_creation_expression
    <- simple_name <- identifier type_argument_list?

因此解析堆栈将包含以下

primary_expression "(" invocation_expression
        ^           ^            ^
        f           (        g<a, b>(c)

前瞻符号将在最后")"之前,因此解析器将减少invocation_expression

argument_list <- argument <- argument_value <- expression
    <- <the same really long list of rules> <- primary_expression
    <- primary_no_array_creation_expression <- invocation_expression

转移最后")"然后会给我们

    primary_expression "(" argument_list ")"
            ^           ^        ^        ^
            f           (    g<a, b>(c)   )

与以前一样,这可以通过invocation_expression规则减少,从而匹配f(g<a, b>(c))

答案 1 :(得分:1)

您可以推迟决定,直到完成解析和/或语义分析(AFAIK C / C ++编译器必须采用后一种方法)。

我写了一篇文章,你怎么能用解析器(在这种情况下为NLT)来做这件事,它允许用不同的解释并行地解析你的输入 - Ambiguity? Let NLT parse it for you。简而言之,你不能确定这是泛型参数的移位还是结束角括号,你可以使用这两个版本,但是等到其中一个版本无效,然后你杀掉它,然后你得到正确的版本。

我没有在这里粘贴全文,因为这样的答案太长了。

答案 2 :(得分:1)

我向the same question询问了Java。

基本上,问题在于:

  • 语言是明确的(至少在这个例子中) - 意味着只有一种正确的解析方法
  • 某些实现会模糊地标记该示例 - 意味着将输入拆分为有效标记的方法不止一种
  • 取决于上下文,正确解析需要不同的标记化 - 比较:

    A<B<C >> d = new A<B<C >> ();
    
    E >> f;
    

我想强调的是,问题不是由语言本身引起的,而是由某些解析它的方法引起的。根据您用来解析它的方式/内容,您可能根本不会遇到此问题。

但是,如果你遇到这个问题,这里有一些通用的解决方案:

  1. 为tokenizer提供足够的有关上下文的信息以正确标记。例如,允许标记化是上下文敏感的而不是常规的,可能通过将标记化与分层分析集成 - 毕竟,单独的标记化传递仅仅是为了效率而不是解析的必要部分的实现细节。使用递归下降解析器很容易实现这种方法。

  2. 模糊地标记,并在有更多上下文信息可用时解决歧义。 (理论上这可能听起来效率低下,但实际上有一些非常快速的实现。)

  3. 执行天真的标记化,但如果需要,可以重新解释标记。这是Java语言的一些解析器显然使用的解决方案,正如my similar question

  4. 中更全面地解释的那样。