可复合语法

时间:2009-06-04 21:23:02

标签: parsing grammar dsl mini-language

有许多编程语言支持包含迷你语言。 PHP嵌入在HTML中。 XML可以嵌入JavaScript中。 Linq可以嵌入C#中。正则表达式可以嵌入Perl中。

// JavaScript example
var a = <node><child/></node>

想想看,大多数编程语言都可以建模为不同的迷你语言。例如,Java可以分为至少四种不同的迷你语言:

  • 类型声明langauge(包指令,导入指令,类声明)
  • 成员声明语言(访问修饰符,方法声明,成员变量)
  • 陈述语言(控制流程,顺序执行)
  • 表达语言(文字,作业,比较,算术)

能够将这四种概念语言实现为四种不同的语法肯定会减少我在复杂的解析器和编译器实现中经常看到的许多意义主义。

我之前已经为各种不同类型的语言实现了解析器(使用ANTLR,JavaCC和自定义递归下降解析器),当语言变得非常庞大和复杂时,你通常会得到一个huuuuuuge语法,并且解析器实现真的很难实现。

理想情况下,在为其中一种语言编写解析器时,最好将其作为可组合解析器的集合实现,并在它们之间来回传递控制。

棘手的是,通常,包含语言(例如,Perl)为所包含的语言(例如,正则表达式)定义其自己的终点标记。这是一个很好的例子:

my $result ~= m|abc.*xyz|i;

在此代码中,主要的perl代码定义了一个非标准的终端“|”用于正则表达式。实现完全不同于perl解析器的正则表达式解析器真的很难,因为正则表达式解析器不知道如何在不咨询父解析器的情况下找到表达式终结符。

或者,假设我有一种允许包含Linq表达式的语言,但不是用分号终止(如C#那样),我想强制Linq表达式出现在方括号内:

var linq_expression = [from n in numbers where n < 5 select n]

如果我在父语言语法中定义了Linq语法,我可以轻松地使用语法前瞻为“LinqExpression”编写明确的生成来查找括号外壳。但是我的父语法必须吸收整个Linq规范。这是一个阻力。另一方面,单独的子Linq解析器将很难确定停止的位置,因为它需要为外部令牌类型实现前瞻。

这几乎排除了使用单独的lexing / parsing阶段,因为Linq解析器将定义一组完全不同于父解析器的标记化规则。如果您一次扫描一个令牌,您如何知道何时将控制权传递回母语词法分析器?

你们觉得怎么样?今天有哪些最佳技术可用于实现不同的,解耦的和可组合的语言语法,以便在较大的父语言中包含迷你语言?

6 个答案:

答案 0 :(得分:4)

您可能希望收听this播客。无扫描解析是“发明”来帮助解决编写不同语法的问题(问题在于你很快发现你不能编写“通用”标记器/扫描器)。

答案 1 :(得分:3)

我正在研究这个确切的问题。我会分享我的想法:

语法很难调试。我在Bison和ANTLR中调试了一些它并不漂亮。如果您希望用户将DSL作为语法插入到解析器中,那么您必须找到一些方法来实现它,这样它就不会爆炸。我的方法是不允许任意DSL,但只允许那些遵循两个规则的人:

  • 文件中所有DSL之间的令牌类型(标识符,字符串,数字)相同。
  • 不允许使用不平衡的括号,括号或括号

第一个限制的原因是因为现代解析器将解析分解为词法阶段,然后应用传统的语法规则。幸运的是,我相信单个通用标记器足以满足您想要创建的90%的DSL,即使它不适合您已经创建的要嵌入的DSL。

第二个限制允许语法彼此更加分离。您可以通过对括号(括号,括号)进行分组然后递归分析每个组来分两个阶段进行分析。嵌入式DSL的语法无法通过其中包含的括号进行转义。

解决方案的另一部分是允许宏。例如,regex("abc*/[^.]")看起来很好。这样宏“regex”就可以解析正则表达式,而不是将正则表达式语法构建成主语言。当然,你不能为你的正则表达式使用不同的分隔符,但你确实在我的脑海中获得了一定程度的一致性。

答案 2 :(得分:1)

解析是问题的一个方面,但我怀疑与每种迷你语言相关的各种可执行解释器之间的互操作可能更难以解决。为了有用,每个独立语法块必须与整体上下文一致地工作(或者最终行为将是不可预测的,因此不可用)。

并非我理解他们真正在做什么,但寻找更多灵感的一个非常有趣的地方是FoNC。他们似乎(我猜)正朝着允许各种不同计算引擎无缝交互的方向前进。

答案 3 :(得分:1)

查看SGLR,Scannerless广义LR解析。以下是一些参考和网址。这种解析技术使得解析表的组合非常简单。特别是与SDF结合使用。

Martin Bravenboer和Eelco Visser。为语言库设计语法嵌入和同化。在软件工程模型:MoDELS 2007研讨会和专题讨论会上,2008年LNCS第5002卷。

MetaBorgMetaBorg in action

答案 4 :(得分:1)

Perl 6可以看作是一组专门用于编写程序的DSL。

事实上,Rakudo的实现就是以这种方式构建的。

即使字符串也是带有可启用或禁用选项的DSL。

Q
:closure
:backslash
:scalar
:array
:hash
"{ 1 + 3 } \n $a @a<> %a<>"

qq"{1+2}" eq 「3」

qq:!closure"{1+2}" eq 「{1+2}」

它基本上必须由可组合语法构成才能实现:

sub circumfix:«:-) :-)» (@_) { say @_ }

:-) 1,2,3 :-)

在Perl中,6个语法只是一种类,而令牌是一种方法。

role General-tokens {
  token start-of-line { ^^ }
  token end-of-line { $$ }
}
grammar Example does General-tokens {
  token TOP {
    <start-of-line> <stuff> <end-of-line>
  }
  token stuff { \N+ }
}

role Other {
  token start-of-line { <alpha> ** 5 }
}
grammar Composed-in is Example does Other {
  token alpha { .. }
}

say Composed-in.parse: 'abcdefghijklmnopqrstuvwxyz';
「abcdefghijklmnopqrstuvwxyz」
 start-of-line => 「abcdefghij」
  alpha => 「ab」
  alpha => 「cd」
  alpha => 「ef」
  alpha => 「gh」
  alpha => 「ij」
 stuff => 「klmnopqrstuvwxyz」
 end-of-line => 「」

请注意,我没有显示一个操作类,这对于在构建解析树时进行转换非常方便。

答案 5 :(得分:0)

如果你考虑一下,这实际上是递归下降解析的工作方式。每个规则及其依赖的所有规则构成一个迷你语法。更高的东西并不重要。例如,您可以使用ANTLR编写Java语法,并将所有不同的“迷你语言”分成文件的不同部分。

这种情况并不常见,因为这些“迷你语言”通常会共享许多规则。但是,如果像ANTLR这样的工具允许您从不同的文件中包含单独的语法,那肯定会很好。这将允许您在逻辑上将它们分开。这可能没有实现的原因可能是它是一个“整容”问题,它纯粹与语法文件本身有关,而不是解析自身。它也不会使您的代码更短(尽管可能稍微容易一些)。这将解决的唯一技术问题是名称冲突。