哪些当代计算机语言是 LL(1)?
Which contemporary computer languages are LL(1)?
(我正在用假期时间研究一些语言理论。如果这是一个幼稚的问题,请原谅。)
根据here:
LL grammars, particularly LL(1) grammars, are of great practical
interest, as parsers for these grammars are easy to construct, and
many computer languages are designed to be LL(1) for this reason.
那么,出于好奇,哪些当代计算机语言是 LL(1)? C、Java、C# 或 Python 是否属于此类?
我想我很想用 [citation needed]; the assumptions are, at least, questionable. For example, there are a large number of compiler-construction tools based on yacc 来标记维基百科的引用,这使得在实践中使用更强大(同样快速)的 LALR 算法构建解析器变得非常容易,并且一些还实现了更强大的(在大多数实用语法中几乎一样快)GLR 算法。几十年来,手写解析器都不是必需的。 [注1]
通过尝试回答问题:
大多数计算机语言 "technically" 不是 LL,因为它们甚至不是上下文无关的。这将包括需要声明标识符的语言(C、C++、C#、Java 等),以及具有预处理器 and/or 宏功能的语言(C 和 C++ 等)、语言含糊不清,只能使用语义信息来解决(Perl 在这里是最糟糕的违规者,但 C 和 C++ 也在那里)。而且,为了让更多人感到快乐,它还包括具有 Python 布局意识的语言(Python,当然还有 Haskell)。
我在 "technically" 周围加上引号,因为有很多人会说这些例外 "don't count"。那是他们的意见,他们有权这样做(无论如何我已经放弃了争论,尽管我不同意那个意见)。例如,您可以通过在应用语法之前预处理文本来消除 C/C++ 中的预处理器,或者通过插入 INDENT、NEWLINE 和 DEDENT 标记而不是空格来预处理可识别空格的语言,之后您可以关于神秘 "core language" 的某种说法。 (这很难应用于 C++ 模板,它只能通过解析文本来消除,因此您不能声称可以在解析之前完成扩展。)
声称一种语言不是上下文无关的,因为它需要声明标识符,这可能更具争议性。在某些语言中(不是 C/C++,其中需要进行语义分析以避免歧义),您可能会争辩说可以在不验证声明规则的情况下构造解析树,这使得该规则 "not syntactic" .但是您仍然可以在语法上强制执行声明规则。你只是不能用上下文无关的语法来做到这一点(例如,你可以使用 Van Wijngaarden grammar)。
无论如何,一种常见的解析策略是识别一种语言的超集,然后通过对生成的解析树的后续分析拒绝不符合要求的程序;在那种情况下,问题就变成了超集是否是 LL。在我看来,为了使它有意义,有必要将每个有效程序解析为正确的语法树,从而消除使用语义分析来消除歧义。
解析的目的是生成语法树,而不仅仅是识别文本是否有效。 (如果你略过侧重于识别的正式语言教科书,你可能会错过这个重要的事实,这可能是因为语法树的构造不太有趣。)因此,坚持建议的语法实际上代表句法结构似乎是合理的语言。
例如,您可以使用简单的正则语言识别代数表达式:
(PREFIX* OPERAND POSTFIX*) (INFIX PREFIX* OPERAND POSTFIX*)*
其中 PREFIX 是前缀运算符集以及 (
,POSTFIX 是 postfix 运算符集(如果有的话)以及 )
,INFIX 是一组中缀运算符(加法、减法、乘法等),OPERAND 是标识符或常量。
那个正则表达式不会正确地拒绝带有不平衡括号的表达式,但我们已经同意识别该语言的超集是可以的,对吧? :-)
如果需要,我们可以从 PREFIX 和 POSTFIX 集中删除括号,并使 OPERAND 成为递归产生式。生成的文法通常是 LL(1) [注 2]:
operand => IDENTIFIER | CONSTANT | '(' expression ')'
term => operand | PREFIX operand | operand POSTFIX
expression => term | term INFIX expression
但是,问题在于此语法不捕获运算符优先级。它甚至没有尝试识别 a+b*c
和 a*b+c
都是和的事实,因此在这两种情况下顶级运算符都是 +
,而不是任何运算符恰好排在第一位在表达中。 (如果您正在解析 APL,这将不是问题。但大多数语言都符合对运算符优先级的通常期望。)
这一点很重要,因为 LL 文法无法处理左递归产生式,而您需要左递归产生式才能正确解析左结合运算符。 (也就是说,正确地将 a-b-c
解析为 ((a-b)-c)
而不是 (a-(b-c))
,后者将具有不同的值。)同样,您可以争辩说这是一个狡辩,因为您可以 post-处理解析树以纠正关联性。这当然没错,但结果是你用来parse的语法和你用来解释语言句法的语法是不一样的。这可能会或可能不会打扰您。
值得在这里补充的是,有一些语言(Haskell、Prolog,可能还有更多),您可以在其中定义运算符及其在程序文本中的优先级。显然,如果不进行语义分析,就不可能生成正确的语法树,解析此类语言的常用方法就是使用上面概述的简化 "alternating operand and operator" 语言。一旦运算符优先级全部已知,大概在解析结束时,然后使用调车场算法之类的东西重新分析所有表达式,以生成正确的解析。
假设我们抛开上述反对意见,只问"for which commonly used programming languages might an LL parser be used?"
不过,为了避免作弊,我们确实应该要求 LL 解析器具有固定的前瞻性并且不需要回溯。如果您允许任意前瞻和回溯,那么您将大大扩展可解析语言的领域,但我认为您不再处于 LL 领域。这将消除,例如,C++ 的模板和预处理器自由子集,即使常见的编译器实现使用递归下降解析器,因为需要回溯才能解决 "Most Vexing Parse" 歧义。 (无论如何,你不能真正从C++中移除模板,用模板解析真的很难。[注3])
Java 和 Python 都设计为 LL(1) "pseudo-parseable"。我相信 Haskell 也属于这一类。没有语义信息就无法正确解析 C(经典歧义:(x)*(y)
是强制转换还是乘法?——这取决于 x
是否已被类型定义或声明为变量)但我是非常确定可以使用非回溯递归下降解析器捕获它。我没有深入研究 C#,但 this answer by Eric Lippert 表明它在某些情况下需要回溯。
备注
当然,人们仍然这样做,而且在很多情况下是有充分理由的(例如,产生更好的错误消息)。但是 "it's difficult to construct an LALR parser" 不是 一个很好的理由,因为它不是。
这不完全是 LL,因为我没有左因子。但这很容易做到;我会把它留作练习。
见Is C++ context-free or context-sensitive?. Also Todd Veldhuizen's classic C++ Templates are Turing Complete
如果我对“计算机语言”的理解比“编程语言”更广泛,我相信您可以考虑几种声明式语言,尽管这也可能取决于您认为当代的语言。
可能的候选人:
- XML
- LLVM IR
- 许多配置语言,如 INI 文件
- 一些网络协议
- 一些数据格式,例如 CSV 文件
大多数(所有?)描述正则表达式的语言都不是正则表达式,而是 LL(1)。
对于实际的编程语言,这些语言可能都太古老了,不能被认为是现代的,而且许多都有可能违反 LL(k) 的共同扩展。
- 汇编语言
- 基础
- LISP
- Wirth 语言:Pascal、Modula、Oberon
- 序言
我对上述所有语言的了解程度还不足以得出结论,这就是为什么我建议它们是 可能 候选者的原因。如果您知道其中任何一个句法歧义,请在评论中告诉我。
(我正在用假期时间研究一些语言理论。如果这是一个幼稚的问题,请原谅。)
根据here:
LL grammars, particularly LL(1) grammars, are of great practical interest, as parsers for these grammars are easy to construct, and many computer languages are designed to be LL(1) for this reason.
那么,出于好奇,哪些当代计算机语言是 LL(1)? C、Java、C# 或 Python 是否属于此类?
我想我很想用 [citation needed]; the assumptions are, at least, questionable. For example, there are a large number of compiler-construction tools based on yacc 来标记维基百科的引用,这使得在实践中使用更强大(同样快速)的 LALR 算法构建解析器变得非常容易,并且一些还实现了更强大的(在大多数实用语法中几乎一样快)GLR 算法。几十年来,手写解析器都不是必需的。 [注1]
通过尝试回答问题:
大多数计算机语言 "technically" 不是 LL,因为它们甚至不是上下文无关的。这将包括需要声明标识符的语言(C、C++、C#、Java 等),以及具有预处理器 and/or 宏功能的语言(C 和 C++ 等)、语言含糊不清,只能使用语义信息来解决(Perl 在这里是最糟糕的违规者,但 C 和 C++ 也在那里)。而且,为了让更多人感到快乐,它还包括具有 Python 布局意识的语言(Python,当然还有 Haskell)。
我在 "technically" 周围加上引号,因为有很多人会说这些例外 "don't count"。那是他们的意见,他们有权这样做(无论如何我已经放弃了争论,尽管我不同意那个意见)。例如,您可以通过在应用语法之前预处理文本来消除 C/C++ 中的预处理器,或者通过插入 INDENT、NEWLINE 和 DEDENT 标记而不是空格来预处理可识别空格的语言,之后您可以关于神秘 "core language" 的某种说法。 (这很难应用于 C++ 模板,它只能通过解析文本来消除,因此您不能声称可以在解析之前完成扩展。)
声称一种语言不是上下文无关的,因为它需要声明标识符,这可能更具争议性。在某些语言中(不是 C/C++,其中需要进行语义分析以避免歧义),您可能会争辩说可以在不验证声明规则的情况下构造解析树,这使得该规则 "not syntactic" .但是您仍然可以在语法上强制执行声明规则。你只是不能用上下文无关的语法来做到这一点(例如,你可以使用 Van Wijngaarden grammar)。
无论如何,一种常见的解析策略是识别一种语言的超集,然后通过对生成的解析树的后续分析拒绝不符合要求的程序;在那种情况下,问题就变成了超集是否是 LL。在我看来,为了使它有意义,有必要将每个有效程序解析为正确的语法树,从而消除使用语义分析来消除歧义。
解析的目的是生成语法树,而不仅仅是识别文本是否有效。 (如果你略过侧重于识别的正式语言教科书,你可能会错过这个重要的事实,这可能是因为语法树的构造不太有趣。)因此,坚持建议的语法实际上代表句法结构似乎是合理的语言。
例如,您可以使用简单的正则语言识别代数表达式:
(PREFIX* OPERAND POSTFIX*) (INFIX PREFIX* OPERAND POSTFIX*)*
其中 PREFIX 是前缀运算符集以及
(
,POSTFIX 是 postfix 运算符集(如果有的话)以及)
,INFIX 是一组中缀运算符(加法、减法、乘法等),OPERAND 是标识符或常量。那个正则表达式不会正确地拒绝带有不平衡括号的表达式,但我们已经同意识别该语言的超集是可以的,对吧? :-)
如果需要,我们可以从 PREFIX 和 POSTFIX 集中删除括号,并使 OPERAND 成为递归产生式。生成的文法通常是 LL(1) [注 2]:
operand => IDENTIFIER | CONSTANT | '(' expression ')' term => operand | PREFIX operand | operand POSTFIX expression => term | term INFIX expression
但是,问题在于此语法不捕获运算符优先级。它甚至没有尝试识别
a+b*c
和a*b+c
都是和的事实,因此在这两种情况下顶级运算符都是+
,而不是任何运算符恰好排在第一位在表达中。 (如果您正在解析 APL,这将不是问题。但大多数语言都符合对运算符优先级的通常期望。)这一点很重要,因为 LL 文法无法处理左递归产生式,而您需要左递归产生式才能正确解析左结合运算符。 (也就是说,正确地将
a-b-c
解析为((a-b)-c)
而不是(a-(b-c))
,后者将具有不同的值。)同样,您可以争辩说这是一个狡辩,因为您可以 post-处理解析树以纠正关联性。这当然没错,但结果是你用来parse的语法和你用来解释语言句法的语法是不一样的。这可能会或可能不会打扰您。值得在这里补充的是,有一些语言(Haskell、Prolog,可能还有更多),您可以在其中定义运算符及其在程序文本中的优先级。显然,如果不进行语义分析,就不可能生成正确的语法树,解析此类语言的常用方法就是使用上面概述的简化 "alternating operand and operator" 语言。一旦运算符优先级全部已知,大概在解析结束时,然后使用调车场算法之类的东西重新分析所有表达式,以生成正确的解析。
假设我们抛开上述反对意见,只问"for which commonly used programming languages might an LL parser be used?"
不过,为了避免作弊,我们确实应该要求 LL 解析器具有固定的前瞻性并且不需要回溯。如果您允许任意前瞻和回溯,那么您将大大扩展可解析语言的领域,但我认为您不再处于 LL 领域。这将消除,例如,C++ 的模板和预处理器自由子集,即使常见的编译器实现使用递归下降解析器,因为需要回溯才能解决 "Most Vexing Parse" 歧义。 (无论如何,你不能真正从C++中移除模板,用模板解析真的很难。[注3])
Java 和 Python 都设计为 LL(1) "pseudo-parseable"。我相信 Haskell 也属于这一类。没有语义信息就无法正确解析 C(经典歧义:
(x)*(y)
是强制转换还是乘法?——这取决于x
是否已被类型定义或声明为变量)但我是非常确定可以使用非回溯递归下降解析器捕获它。我没有深入研究 C#,但 this answer by Eric Lippert 表明它在某些情况下需要回溯。
备注
当然,人们仍然这样做,而且在很多情况下是有充分理由的(例如,产生更好的错误消息)。但是 "it's difficult to construct an LALR parser" 不是 一个很好的理由,因为它不是。
这不完全是 LL,因为我没有左因子。但这很容易做到;我会把它留作练习。
见Is C++ context-free or context-sensitive?. Also Todd Veldhuizen's classic C++ Templates are Turing Complete
如果我对“计算机语言”的理解比“编程语言”更广泛,我相信您可以考虑几种声明式语言,尽管这也可能取决于您认为当代的语言。
可能的候选人:
- XML
- LLVM IR
- 许多配置语言,如 INI 文件
- 一些网络协议
- 一些数据格式,例如 CSV 文件
大多数(所有?)描述正则表达式的语言都不是正则表达式,而是 LL(1)。
对于实际的编程语言,这些语言可能都太古老了,不能被认为是现代的,而且许多都有可能违反 LL(k) 的共同扩展。
- 汇编语言
- 基础
- LISP
- Wirth 语言:Pascal、Modula、Oberon
- 序言
我对上述所有语言的了解程度还不足以得出结论,这就是为什么我建议它们是 可能 候选者的原因。如果您知道其中任何一个句法歧义,请在评论中告诉我。