LBNF语法的第一个示例
作为LBNF的第一个例子,考虑以下语法仅限于加一的表达式:
EPlus公司。Expr公司::=Expr“+”数字;ENum(欧洲)。Expr公司::=编号;没有。编号::="1" ;
除了标签 EPlus公司
,ENum公司
、和没有
,规则很普通BNF规则,带双引号和未加引号的非终结符。标签用作构造函数用于语法树。
BNF Converter从LBNF语法中提取抽象语法和一个具体语法例如,在Haskell中,抽象语法实现为数据类型定义系统:
数据 Expr公司 = EPlus公司 Expr公司 编号 | ENum公司 编号
数据 编号 = 没有
对于其他语言,如C、C++和Java,等效表示为根据Appel系列丛书中定义的方法现代ML/Java/C中编译器的实现 。具体语法为由lexer、parser和pretty-printer算法实现在其他生成的程序模块中定义。
简而言之,LBNF
基本LBNF
简而言之,LBNF文法是BNF文法,其中每个规则都有一个标签。标签用于构建抽象语法树,其子树由规则的非终结符以相同的顺序给出。
更正式地说,LBNF语法由一组规则组成,这些规则具有以下形式(用正则表达式表示附录:LBNF规范给出了符号的完整BNF定义):
标识符“。”标识符“::=“(标识符|字符串)*”;"
第一个标识符是规则标签,然后是价值类别。在右侧生产箭头(::=
)是生产项目的列表。项目是带引号的字符串(终端)或类别符号(非终端). 值类别为的规则也是称为生产对于.
可以选择标识符,即规则名称和类别符号广告自由,受目标语言的限制。收件人如果满足C、Haskell和Java,则会强制执行以下规则:
标识符是由字母、数字和下划线,以大写字母开头,不以下划线结尾。
其他功能
上面定义的基本LBNF显然足以定义任何无上下文语言。然而,定义纯粹使用BNF规则的编程语言。因此,一些额外的LBNF中增加了一些功能:抽象语法约定、lexer规则、,杂注和宏。这些特征将在随后的部分中进行处理部分。
章节抽象语法约定解释抽象语法约定.创建通过为每个BNF规则添加节点类型的抽象语法可以有时变得过于详细,或者被额外的结构弄得乱七八糟。收件人为了解决这个问题,我们已经确定了最常见的问题案例,并添加了给LBNF一些额外的约定来处理它们。
章节Lexer定义解释lexer规则.语言的某些方面属于它的词汇结构而不是语法由正则表达式而非BNF规则自然描述。我们有因此,在LBNF中添加了两种规则格式来定义词法结构:代币和评论.
章节LBNF杂注解释杂注.语用是指导BNFC语法编译器处理某些语法规则特殊方法:减少入口点或者治疗一些句法形式为内部的只有。
章节LBNF宏解释宏.宏是的语法糖潜在的大量规则,有助于简洁地编写语法。这是为了作者和读者的方便;除此之外事物、宏自然会强制某些规则组合在一起,否则会在语法中任意传播。
章节布局语法解释布局语法,这是一个非文本自由某些编程语言中存在的功能。LBNF有一套规则用于定义有限形式的布局语法的格式。它作为将布局语法转换为显式结构的预处理器标记。注意:只有Haskell后端(包括Agda)实现布局功能。
抽象语法约定
预定义的基本类型
第一个约定是预定义的基本类型。基本类型,例如整数和字符,当然可以在LBNF中定义,用于例子:
然而,这既麻烦又低效。相反,我们决定用预定义的基本类型扩展我们的形式主义,并表示它们语法是词汇结构的一部分。这些类型如下:,由LBNF正则表达式定义(请参见Lexer定义对于普通人表达式语法):
类型整数
整数,定义数字+
类型双精度
浮点数的数量,已定义数字+ '.' 数字+ (“e” '-'? 数字+)?
类型字符
个字符(用单引号括起来),已定义'\'' ((字符 - ["'\\"]) | ('\\' [“'\\tnrf”])) '\''
类型字符串
字符串(双引号),已定义'"' ((字符 - ["\"\\"]) | ('\\' [“\”\\tnrf“]))* '"'
类型标识
(Haskell)标识符的信 (字母 | 数字 | '_' | “\”)*
在抽象语法中,这些类型表示为相应的每种语言的类型,除了标识
不存在此类类型的。它被视为新类型
对于字符串
在哈斯克尔,
作为字符串
在Java中,作为类型定义
到字符*
C和香草C++,和作为类型定义
到标准::字符串
在C++中使用标准模板库(STL)。
正如类型名称所示,lexer可以产生高精度整数和浮点的变量。应用程序的作者可以截断如果他们想改为低精度,则可以稍后使用这些数字。
注释
规则中出现的端子优先于标识
例如。,if端子“在哪里”
出现在任何规则中,单词哪里
将永远不会被解析为标识
.
语义假人
有时,语言的具体语法包括不语义差异。一个示例是使解析器接受的BNF规则语句后的额外分号:
由于此规则是语义no-op,因此我们不想用抽象语法中的构造函数。相反,我们介绍以下内容约定:
规则标签可以是下划线_,它不会添加语法树中的任何内容。
因此,我们可以在LBNF中编写以下规则:
下划线当然只有在替换值类型与参数类型。语义假人在漂亮的打印机上没有留下任何痕迹。因此,例如,漂亮的打印机会额外“正常化”分号。
优先级
(普通)BNF中的一个常见习惯用法是使用表示优先级的类别:
实验2::=整数;实验1::=实验1“*”实验2;费用::=实验“+”实验1;实验2::=“(“Exp”)”;实验1::=实验2;费用::=实验1;
优先级别调节解析的顺序,包括关联性。括号将任何级别的表达提升到最高级别。
对上述规则进行简单的标记可以创建一种语法具有所需的识别行为,如抽象语法所示类型区分杂乱(介于费用
,实验1
、和实验2
)和没有语义内容的构造函数(来自最后三个规则)。这个BNF转换器解决方案是区分类别符号它们只是彼此的索引变体:
类别符号可以结束使用整数索引(即数字序列),然后进行处理作为相应的无索引符号的类型同义词。
因此,实验1
和实验2
是的索引变体费用
.
索引变体之间的转换是语义上的no-op,我们确实如此不想用抽象语法中的构造函数来表示它们。为了实现这样,我们将下划线的使用扩展到索引变体。这个例子上面的语法现在可以标记如下:
EInt公司。实验2::=整数;E时间。实验1::=实验1“*”实验2;EPlus公司。费用::=实验“+”实验1;_. 实验2::=“(“Exp”)”;_. 实验1::=实验2;_。费用::=实验1;
例如,在Haskell中,表达式的数据类型变得简单
数据 费用 = EInt公司 整数 | E时间 费用 费用 | EPlus公司 费用 费用
和的语法树2 * ( 三 + 1 )
是
E时间 (EInt公司 2) (EPlus公司 (EInt公司 三) (EInt公司 1))
索引的类别可以用于优先权以外的其他目的,因为我们唯一可以正式检查的是类型骨架(参见部分LBNF规则的类型正确性). 解析器不需要知道索引平均优先级,但只有索引变量的值相同类型。然而,pretty-printer假设索引类别用于优先级,如果在其他方式。
提示
参见第节胁迫定义虚拟对象的简明方法强制规则。
多态列表
在LBNF中定义单态列表类型很容易:
零定义。列表定义::=;宪法定义。列表定义::=定义“;”列表定义;
然而,像Haskell这样的语言的编译器编写者可能希望使用预定义的多态列表,因为这些列表的语言支持构造。LBNF允许使用Haskell的列表构造函数作为标签和类别名称中的列表括号:
[]. [定义]::=;(:). [定义]::=定义“;”[Def];
一般来说,我们有
[中]
,类型列表的类别C类
,
[]
和(:)
、零和Cons规则标签,
(:[])
,单元素列表的规则标签。
第三个规则标签是用于设置一个东向限制,但也允许特殊在具体语法中处理单元素列表。
LaTeX文档(出于风格原因)和Happy文件(针对句法原因),类别名称[中]
被替换为列表C
.为了防止冲突,列表C
不能同时使用在语法中明确表示。
列表符号也可以看作是克莱恩星和以及因此作为来自Extended BNF的成分。
一些编程语言(如C)没有参数多态性。然后,各个后端生成单态列表的变体。
提示
参见第节终端和分离器定义列表的简明方法只是给他们的终结符或分隔符。
LBNF规则的类型正确性
在解析器生成器中,通常会委托某些目标语言错误。例如,一个Happy源文件没有投诉的愉快流程仍然可以生成Haskell文件被Haskell拒绝。同样,BNF转换器委托对生成的语言进行一些检查(例如,解析器冲突检查)。然而,由于这对于程序员理解与源BNF相关的错误消息转换器执行一些检查,这些检查主要与抽象语法的健全性。
类型检查器使用类别骨架或类型一条规则,哪种形式
哪里是未编制索引的左侧非终结符是(可能为空)无索引右侧序列规则的非终结符。换句话说规则表示语义动作的抽象语法类型与该规则关联。
我们还需要常规类别和一个常规规则标签。简单地说,常规标签和类别是用户定义的个。更正式地说,常规类别不是[中]
,整数
,双精度
,字符
,字符串
和标识
,或类型由定义代币
规则(第节令牌规则). 常规规则标签为无属于_
,[]
,(:)
、和(:[])
.
类型检查规则如下:
由标记的规则_
必须具有形式的类别骨架.
由标记的规则[]
必须具有形式的类别骨架.
由标记的规则(:)
必须具有形式的类别骨架.
由标记的规则(:[])
必须具有形式的类别骨架.
只有常规类别才能具有具有常规规则标签的产品。
语法中出现的每个常规类别必须至少有一个使用常规规则标签进行生产。
具有相同常规规则标签的所有规则必须具有相同类别骨架。
第二条规则对应于没有空数据Haskell中的类型。最后一条规则可以加强,以便要求所有常规规则标签都是唯一的:这需要保证无差错的精美印刷。当前违反此强化规则只生成警告,而不是类型错误。
定义的标签
LBNF支持已消除的语法树构造函数在解析期间,通过以下方式语义定义。这是一个示例:核心语句语言:
分配。Stm公司::=标识“=”Exp;区块。Stm公司::=“{”[Stm]“}”;与此同时。Stm公司::=“while”“(”Exp“)”Stm;如果Stm::=“if”“(”Exp“)”“Stm”else“Stm;
我们现在想要一些语法糖。请注意,这些标签所有规则都以小写字母开头,表示它们对应到定义的函数而不是抽象语法树中的节点。
如果Stm::=“if”“(”“Exp”“)”“Stm”“endif”;的。Stm公司::=“对于”(“Stm”;“Exp”;“Stm”)“Stm;包括Stm::=标识“++”;
函数使用定义
关键字。定义具有形式:
哪里e(电子)
是使用规则标签的应用形式的表达式,其他定义的函数、列表和文字。
定义如果e s=如果e s(块[]);定义i c s b=块[i,而c(块[b,s])];define inc x=赋值x(EOp(EVar x)+(EInt 1));终止符Stm“;”;
定义函数的另一个用途是简化二进制运算符。我们希望每个操作符都有一个节点,而不是一个节点所有二进制运算符应用程序的通用节点(EOp)。
_。操作::=操作1;_. 操作::=操作2;更少。操作1::="<" ;相等。操作1::="==" ;另外。操作2::="+" ;减。操作2::="-" ;营业费用::=实验1操作1实验1;操作实验1::=实验1操作2实验2;EInt公司。实验2::=整数;EVar公司。实验2::=标识;胁迫实验2;
必须小心确保漂亮的打印机输出足够圆括号。
内部EOp。费用::=实验1操作实验1;定义ope1 o e2=EOp e1 o e 2;
注释
通过实现语法糖定义
也许对快速运动有好处原型,但对于更严肃的编程来说有一些缺点语言实现:由于解析器已经扩展了语法糖,前端的后续部分,如范围或类型检查器,不要报告糖中的错误用户可能已经写了,但什么会到达检查器。对于实例,xs公司++
对于数组变量X轴
不会报告中的问题X轴++
,而是在扩张中x个 = x个 + 1
.
LBNF宏
终端和分离器
这个终止器
宏通过什么标记定义了一对列表规则终止列表中的每个元素。例如,
告诉每个语句(Stm公司
)以分号结尾(;
). 这是这对规则的简写
[]. [标准]::=;(:). [标准]::=Stm“;”[Stm];
限定符非空的
在宏中,使一个元素列表成为基本情况。因此
是的缩写
(:[]). [标准]::=Stm“;”;(:). [标准]::=Stm“;”[Stm];
终止符可以指定为空""
。未引入令牌然后,但是例如。
被翻译为
[]. [标准]::=;(:). [Stm]::=Stm[Stm];
这个分离器
宏类似于终止器
,除了分隔标记未附加到最后一个元素。因此
方法
[]. [标准]::=;(:[]). [标准]::=Stm;(:). [标准]::=Stm“;”[Stm];
然而
方法
(:[]). [标准]::=Stm;(:). [Stm]::=Stm“;”[Stm];
注意,如果空标记""
使用,没有区别之间终止器
和分离器
.
问题。从分离器
没有非空的
实际上也会接受以分号,而漂亮的打印机将其“正常化”。这可能会被认为是一个错误,但有一套规则禁止终止分号会更复杂:
[]. [标准]::=;_. [标准]::=[标准]1;(:[]). [标准]1::=标准时间;(:). [标准]1::=Stm“;”[Stm]1;
不幸的是,这不是合法的LBNF,因为不支持列表。
胁迫
这个强制转换
宏是一组规则翻译的简写优先级别之间。例如,
是的缩写
_. 费用::=实验1;_. 实验1::=实验2;_. 实验2::=实验3;_. 实验3::=“(“Exp”)”;
由于这些胁迫的全部覆盖范围,是否表示最高级别的整数(此处三
)大于实际发生的最高级别,或是否存在其他级别语法中没有结果。然而,未使用的级别可能会使生成的解析器定义文件膨胀(例如,Haskell的Happy文件),除非后端实现某种死码删除。(截至2020年10月,没有后端实现了如此巧妙的分析。)
提示
强制类别(例如。实验2
)也可以用于其他规则。对于实例,给定以下语法:
EInt公司。实验2::=整数;E添加。实验1::=实验1“+”实验2;
你可能想用实验2
而不是简单地费用
强制使用非平凡表达式的括号。例如,富。 F类 ::=
“foo” 实验2 ;
将接受foo公司 42
或foo公司 (1 + 1)
但是不
foo公司 1 + 1
.
您甚至可以在列表中使用强制类别,并为它们提供不同的分隔符/终止符:
规则
这个规则
宏是一组规则的简写,标签来自这些规则自动生成。例如,
规则类型::=Type“[”Integer“]”|“float”|“double”| Type“*”|Ident;
是的简写
类型1.类型::=键入“[”Integer“]”;类型_浮动。类型::=“浮动”;键入double。类型::=“双重”;类型2.类型::=键入“*”;类型标识。类型::=标识;
标签是自动创建的。标签以值开头类别名称。如果产品只有一个项目作为标识符的一部分,该项可能用作后缀。在其他情况下,使用整数后缀。未执行全局检查生成这些标签时。BNFC类型捕获导致类型错误的标签名称冲突检查生成的规则。
布局语法
布局语法是一种使用缩进对程序元素进行分组的方法。它在一些语言中使用,例如Agda、Haskell和Python。布局语法是BNFC的一个相当实验性的特性由Haskell系列后端支持,因此读者第一次阅读时可能会跳过这一节。
杂注布局
,布局 停止
、和布局 顶层
定义一个布局语法对于一种语言。从这些杂注中,一个名为布局
,布局解析器,被创建为插入lexer和解析器之间。这个模块将额外的标记(大括号和分号)插入到标记中根据下面解释的规则,流来自lexer。
提示
为了获得其他后端的布局分辨率,生成的Haskell布局解析器可以手动输出到所需的编程语言。或者,生成的Haskell lexer和布局解析器可以变成一个独立的预处理器将文件插入标记,根据布局规则,并将标记打印回字符lexer和解析器可以进一步处理的流用所需的编程语言编写。
的布局杂注BNFC的功能不足以处理Haskell的完整布局规则98,但对于“常规”情况,它们就足够了。
关于功能概述,请看第一个简单的示例:树语法。A类树
是一个数字(有效载荷),后跟br个
加上一个子树列表(可能为空):
节点。树::=整数“br”“{”[树]“}”;分隔符树“;”;布局“br”;
列表用大括号括起来,并用分号分隔。这是布局机制目前唯一支持的语法。
生成的解析器可以处理以下示例:
0 br1个br2个br3个br4 br5 br6 br7 br
由于布局分辨率,这与将在不支持布局的情况下处理以下输入:
0 br{1 br{2br{}; 3个br{}}; 4 br{5 br{6br{}}}; 7个br{}}
布局解析器更精确地生成以下等价项中间文本(以令牌流的形式):
0 br{1个br{2 br{}; 3个br{}}; 4 br{5 br{6 br{}}}; 7 br{}}
现在来看一个更现实的例子,它位于逻辑框架的语法中阿尔法。(注意:这个例子有点过时了……)
布局“of”、“let”、“where”、“sig”、“struct”;
第一行说“第个,共个”
,“让”
,“其中”
,“信号”
,“结构”
是布局词,即启动布局列表。布局列表是表达式列表,通常用花括号括起来,用分号,如Alfa示例所示:
E案例。费用::=“{”“[分支]”“}”的case“Exp”;分隔符分支“;”;
当布局解析器找到标记时属于
在代码中(即在它检查下一个标记是否是打开花括号。如果是这样的话,在布局之前不会做任何特别的事情再次遇到单词。解析器需要分号和右括号以正常显示。
但是,如果代币下列的属于
不是开口卷曲括号,插入括号(或当前布局列之后的列,以较大者为准)被记住为布局列表元素必须位于的位置开始。分号插入这些位置。当令牌为最终遇到的位置左侧(或一个文件末尾),在该点插入一个右括号。
允许嵌套布局块,这意味着布局解析器保持一堆位置。将位置推到堆栈上对应于插入左括号,然后从堆栈中弹出对应于插入右括号。
以下是使用布局的Alfa源文件示例:
c::Nat=案例x真->bFalse->第y种情况错误->b两者都不超过dd=真的情况x->假的情况y->gx->by->小时
以下是布局分辨率后的外观:
c::Nat=案例x{正确->b;False->第y种情况{错误->b};两者都不超过d};d={True->case x of{False->g;x->b};y->h};
还有两个与布局相关的杂注。布局停止杂注,如
告诉解析器可以通过一些停止退出布局列表单词,比如在里面
,退出让
列表。分解器中没有错误退出其他类型的布局列表在里面
,但将显示错误在解析器中。
布局顶层
pragma告诉我们整个源文件是一个布局列表中,即使没有布局词表明这一点。位置是第一列,解析器在每个段落后添加分号他的第一个标记在这个位置。没有添加花括号。这个上面的Alfa文件就是一个例子,添加了两个这样的分号。
堆叠布局关键字
自2.9.2版起,布局处理器支持布局堆叠同一行上的关键字。考虑以下片段阿格达语语法:
模块。下降::=“模块”Ident“其中”“{”“[Decl]”“}”“;私人。下降::=“私人”“{”[拒绝]“}”;类型Sig。下降::=Ident“:”Ident;分隔符Decl“;”;布局“where”,“private”;
此语法允许我们定义包含声明列表,每个声明都可以是模块、私有块或类型签名。声明列表用大括号括起来和由分号分隔,并由布局关键字引入哪里
和私有的
.
我们可能想定义一个私有模块,而不是随后的两个缩进:
我们希望将两个布局介绍堆叠在一行上:
BNFC 2.9.1之前的布局逻辑不支持这一点,因为我们将修复的布局列私有的
块到第8列标记列模块
.令牌A类
遵循下一个布局关键字哪里
位于低于8的第2列,因此私有的
这里的街区将被关闭。但是有私有的
关闭,内部块模块 M(M) 哪里
也需要关闭,并且然后是声明A类 : 设置
只是被误解了。因此,这给出了BNFC 2.9.1的解析错误。
BNFC 2.9.2生产的布局处理器标记布局第8列私有的
块作为实验性的因为代币模块
遵循layout关键字私有的
是一样的行。相反模块
块是决定性的因为代币A类
遵循layout关键字哪里
在后面一行。检查新布局时列有效,临时布局列被忽略,因此它只是检查2>0,而不是2>8。有时指向模块
块将被关闭,因为我们遇到令牌缩进小于2私有的
街区也将关闭因为它的压痕列8也比遇到的标记。
通常,当遇到新行时,我们会切换顶部布局列到决定性的如果他们还在实验性的例如,考虑以下情况:
在第一行的末尾,我们有两个暂定布局列,8(第列,共列模块
)和23(第列,共列A类
). 从现在起我们没有挂起这行中的layout关键字,我们切换布局列到决定性的当我们看到下一个标记时模块
在一条新线路上。第一个模块到此结束,留给我们明确的布局第8列。这可以防止我们分配给后续块模块 N个
带有布局关键字哪里
列2(标记坏
). 相反,此块获得默认列8+1。什么时候?处理令牌坏
两个区块由于2<8和2<8+1而关闭。但后来坏
是错误,产生预期的解析错误。
为引入的更改堆叠布局关键字是向后的在以下意义上兼容:由生成的布局软件解析器BNFC 2.9.2接受所有被BNFC 2.9.1及之前版本生成的相应解析器。此外为这些接受的文本生成相同的语法树。
优化:左递归列表
列表的BNF表示是右递归的,紧跟在列表之后Haskell和大多数其他语言中的构造函数。右递归列表,然而,在移位减少解析器中需要线性堆栈空间(例如LR解析器系列)。当堆栈受到限制(例如野牛
生成的解析器)。
右递归列表定义
[]. [标准]::=;(:). [标准]::=Stm“;”[Stm];
在左递归转换下变为左递归:
[]. [标准]::=;(翻转(:))。[标准]::=[Stm]Stm“;”;
然而,当作为另一个规则的一部分使用时,需要反转这样解析的列表。
对于目标为堆栈受限解析器(C、C++、Java)的后端,BNF转换器自动执行左递归形式规则对的变换
[]. [中]::=;(:). [中]::=C x[C];
其中C是任何类别,x是终端的任何序列(可能空)。当然,这些规则可以从终止符生成宏(节终端和分离器).
通知。如果单元素列表是基本情况。它也不在Haskell后端中执行,该后端使用堆分配堆栈通过幸福的
.