问候,
这周我们讨论小语言的句法方面的设计。
它有助于识别此类语法的解析器的设计。 上个星期
我们看到了令牌化器:它只是将字节序列分组为令牌; 它没有
完全不了解任何语言的语法。 那是工作
解析器 另一方面,解析器不在乎这些令牌的来源,
也不知道它们是如何形成的。 解析器和分词器紧密协作,顺序良好
了解并检查特定输入流的正确性。 但首先,
让我们设计我们的小语言。
一种小型编程语言我希望这种语言尽可能小,因为我不想写
整本关于这个主题的小说。 我们的小语言将是一小段表达
语言。 它将翻译将被解释的表达式序列
由产生这些表达式的值的解释器。
语言也必须了解变量,函数,特殊函数
作为“原子”部分,即浮点数和简单列表。 必有
是内置函数以及用户定义的函数。 内置列表
如果要扩展此语言,则函数必须易于扩展。
用我们的小语言从语法上讲一个程序看起来像这样:
program: ((definition | expression) ';')*
内容为:程序是以下零个或多个的列表:a
定义或表达式,后跟“;” 字符。 拖尾星
表示括号中的前一个序列出现0次或多次。
竖线表示一个选择:左侧的零件或零件
在垂直栏的右侧。
“程序”一词被称为语法/语法的“规则”。 “定义”和
“表达”也是规则; 我们只是还没有定义它们; 任何东西
规则定义中任何地方的引号是令牌生成器提供的令牌。
';' 是一个简单的令牌。 括号用于对规则的各个部分进行分组
一起| 和*具有特殊含义(请参见上文)。 +加号
符号也有特殊含义,就像*星:星代表零
或上一个规则部分中的一个或多个,加号表示一个或多个上一个规则部分
规则部分。 如果我是这样写的:
program: ((definition | expression) ';')+
如果一个程序至少包含一个,则在语法上是正确的
定义或表达。 当程序可以为空时,我喜欢它,所以我使用了
星号(零个或多个定义或表达式)。 还有另一个特别之处
符号:问号; 一个 ? 指示应该发生前一部分
最多一次 所以如果我写了这个:
program: ((definition | expression) ';')?
语法上有效的程序可以为空,也可以为一个定义,或者
允许表达。 就我们的目的而言,这太过严格了。
EBNF表示法上面的语法描述称为“ EBNF” :(扩展Backus-Naur格式)。
John Backus发明了这种表示法,Peter Naur稍后对其进行了简化。
两者都受到Noam Chomsky的工作的启发,他提出了一个全新的概念。
语言的思考方式。 在1950年代后期的某个地方,他发表了他的
工作,并立即被古典语言学家所讨厌,他认为
语言的“感觉”和“直觉”将不再存在。
诺姆·乔姆斯基(Noam Chomsky)是一位固执的科学家,根本不在乎愤怒
他因工作而受到批评,并认为语言学从未如此
在他发表作品之前先学习一门适当的科学。
乔姆斯基更进一步指出,每个人天生都是自然的
通用语法,所有语言都是该通用语言的派生词
语法。 蹒跚学步的孩子要做的就是弄清楚它们的特殊性
他母亲的语言是为了学习如何说和形成句子
在语法上是正确的。 这种烙印的通用语法逐渐消失
当人类长大。 那个现象可以解释为什么小孩子学习
第二语言的速度如此之快(只需将其与通用语法相匹配),
大人要掌握另一种语言时就必须努力学习。
扩展Backus-Naur格式将语法的这一概念形式化。
定义再次回到我们的编程语言:该语法中提到的定义
规则实际上可以是函数定义或函数声明。
我们需要一个声明的概念,因为我们的解析器是“一次通过”解析器,
即,它一次读取令牌流,然后必须完成所有工作。
当解析器“看到”由令牌生成器传递的名称时,它不知道
但是该名称代表一个用户函数,它将引发错误:它
无法将该名称识别为用户功能。 如果两个函数分别调用
其他必须递归声明两个函数。 但是使用了一种功能
在其他功能的主体中,反之亦然。 一个功能体定义
必须先行,这就是为什么我们需要声明; 考虑一下:
function foo(x) = bar(x-1);
function bar(x) = foo(x/2);
当解析函数foo的函数体时,关于bar的信息一无所知
然而。 但是我们不能先定义功能栏,因为它指向功能foo
再次。 使用声明,我们可以解决这个小问题22:
function bar(x);
function foo(x) = bar(x-1);
function bar = foo(x/2);
第一行声明功能栏的存在,但尚未定义。
第二行声明并定义了一个使用函数bar和
第三行最后通过提供功能栏来定义功能栏。 注意
当已经声明一个函数时(第一行),不允许重复
再次使用正式参数列表,即正式参数列表已经
在函数的声明中定义。
这也允许使用更简单的解析器,因为它不必检查是否
两个形式参数列表是否相等,它们当然必须相等。
还要注意,我引入了“功能”一词; 这是保留字; 另一个
保留字将在稍后偷偷引入。 没有用户定义的功能
变量也不能命名为“函数”。 唯一的目的是介绍
函数声明或定义的开始。
请注意,在函数go中声明并定义了函数foo的第二行是
有时称为“暂定”定义。 该函数都被声明并且
它的形式参数列表以及函数本身都已定义。
还要注意,其他编程语言使用不同的语法,但是我们
定义自己的小语言,我们就可以自由地做自己想做的事。
上面所有这些东西在EBNF表示法中是什么样的? 看看这个:
definition: ( 'function' | 'listfunc' ) 'name' defordecl
defordecl: ('(' paramlist ')' ( '=' expression )?) | ( '=' expression )
paramlist ( 'name' ( ',' 'name' )* )?
第一行很简单:它只是读取关键字'function'或'listfunc'
需要后面跟一个“名称”,再跟另一个规则defordecl。 德福
规则定义函数声明或函数的(暂定)定义。
我们要么解析一个参数列表,然后可选地后面跟一个'='和一个表达式
或者我们跳过参数列表,并期望立即有一个'='和一个表达式。
参数列表可以是“名称”,后跟其余的参数列表
或列表完全为空。 参数列表的其余部分是一个序列
一个“,”后跟另一个“名称”的次数为零或多次。
现在,我们略微忽略“ listfunc”保留字; 它表明
用户定义的函数将整个列表作为其参数。 我们将回到它。
我们已经为大多数我们的小语言设计了语法。 有一部分
仍然缺少:表达式的语法:
表达我们想解析普通的,易读的表达式。 表达式包括
二进制和一元运算符以及变量名,函数
用户定义(请参见上文)或内置函数。 我们也希望能够
解析列表。
让我们从二进制运算符表达式开始。 一些二进制运算符绑定更多
它们的操作数比其他操作数更紧密,例如,我们期望4 + 2 * 19等于42
不是114。我们希望首先进行乘法运算,然后再进行加法运算。
乘法运算符的优先级高于加法运算符。
我们的小语言知道六个不同的优先级。 从最低到
运算符的最高优先级是:
:
== !=
<= < > >=
+ -
* /
^
从鸟瞰来看,一个表达只是一个或多个其他表达
中间带有':'标记。 当只有另一种表达时
表达式中没有':'标记。 “其他表达方式”
不包含令牌“:”。 这是此规则的EBNF形式:
expression: comparison ( operator0 comparison )*
operator0: ':'
':'运算符只是表达一系列表达式的一种方式
只是一个 表达式的值是右边的表达式
它; 忘了':'标记左侧表达式的值。
相等表达式测试(in)相等的另外两个表达式:
expression: comparison ( operator1 comparison )*
operator1: '==' | '!='
比较看起来与顶级表达式没有什么不同:
comparison: addition ( operator2 addition )*
operator2: '<=' | '<' | '>' | '>='
而且加法表达式也看起来类似:
addition: multiplication ( operator3 multiplication )*
operator3: '+' | '-'
已经有点无聊了。
这是乘法的EBNF:
multiplication: power ( operator4 power )*
operator4: '*' | '/'
最后,这里是处理具有以下内容的二进制表达式的表达式:
最高优先级:
power: unary ( operator5 unary )*
operator5: '^'
如果我已命名表达式,比较,加法,乘法和幂
规则不同,我们将得到以下信息:
expression0: expression1 ( operator0 expression1 )*
expression1: expression2 ( operator1 expression2 )*
expression2: expression3 ( operator2 expression3 )*
expression3: expression4 ( operator3 expression4 )*
expression4: expression5 ( operator4 expression4 )*
expression5: unary ( operator5 unary )*
operator0: ':'
operator1: '==' | '!='
operator2: '<=' | '<' | '>' | '>='
operator3: '+' | '-'
operator4: '*' | '/'
operator5: '^'
当我们要为我们的开发实际代码时,请记住这个重命名技巧
二进制表达式解析器。 接下来是“一元”表达式。 一元表达式
不像二进制表达式那么规则。 一元表达式是一元
运算符后跟一元表达式,或者它是不带任何表达式的表达式
领先的一元运算符:
unary: ( unaryop unary ) | atomic
unaryop: '+' | '-' | '!'
原子表达式是以下之一:
-括号中的表达式(嵌套)或
-函数调用(function)或
-名称(可选)后跟一个赋值(nameexpr)或
-常数(常数)或
-表达式列表(列表)
括号中的表达式就是这样,用EBNF表示法:
nested: '(' expression ')'
这是函数调用的EBNF表示法:
function: 'name' '(' params ')'
params: ( expression ( ',' expression )* )?
请注意参数规则与形式参数列表的规则的相似性
(有关该规则,请参见上面的定义部分)。
赋值(或只是变量名)如下所示:
nameexpr: 'name' ( ( assignop expression ) | '++' | '--' )?
assignop: '=' | '+=' | '-=' | '*=' | '/=' | '^='
如果一个赋值运算符之一后没有名称,我们只想
使用以“名称”表示的变量的值; 否则我们想
使用中提到的表达式为变量分配另一个值
第一法则。 最后,名称后可以带有一元++或-后缀表达式。
常量就是它的含义:它是一个浮点常量,可被
分词器; 没有有趣的EBNF表示法; 仅此一个:
constant: 'constant'
最后,列表由零个或多个表达式组成,并用逗号和
括在大括号中:
list: '{' ( expression ( ',' expression )? )* '}'
这是我们小的编程语言的整个语法。
原子的
表达的东西是最大的混乱,解析器必须检查几个
它解析一个“名称”时的事情,即它可以是一个简单的变量名,但是
它也可以是内置函数的名称,也可以是
用户定义的功能。 它也可能是作业的第一部分。
为了弄清楚解析器全都跟踪内置列表
功能以及用户定义功能的映射。 但是,正如他们所说,
只是一个“实施细节”,我们将其留给我们
开始实现我们的解析器。
解析器生成器我们正在手工制作解析器; 如今不再需要:
很少有工具可以为您做到这一点。 你喂它简单
包含语法的EBNF表示法的文本文件,该工具会生成
整个解析器在片刻之内。 词法分析器生成器也是如此:
当您输入一堆常规的词时,它们会生成整个词法分析器
表达式。 我们不会走那条路; 我们没有走那条路
我们的标记器(词法分析器),我们也不会走这条路
为我们的解析器。 我们将以艰难的方式做到这一点,只是为了展示
这些野兽的复杂性就在这里。
结束语在本部分中所有这些理论知识之后
在编译器文章中,看看我们的语言可以做什么:
function fac(n) =
if ((n < 2)
, 1
, n*fac(n)
);
这定义了一个函数“ fac”,用于计算参数“ n”的能力。
我们甚至可以用数字列表来做到这一点:
fac( { 1, 2, 3, 4, 5, } );
结果将是{1,2,6,24,120}。
列表以及一些特殊的
内置函数将使我们的小程序设计语言相当丰富
好玩
实话实说,我已经实现了大部分功能:令牌生成器,
解析器,代码生成器和解释器以及所有用具
系统需要它,并且确实很有趣。 也许
这种小语言在某处找到了用处; 谁知道? 也许我会想出
一个很好的应用程序。
下周,我们必须进行一些簿记:解析器的表
然后将说明令牌生成器。 它显示了一些有趣的用法
资源类:它处理原始文件资源并为
要填充的表。原始文件资源只是Properties对象,
但是加上一点String摆弄,他们可以做相当高级的事情,
由该Resource类进行。
请继续关注,下周见
亲切的问候,
乔斯
From: https://bytes.com/topic/java/insights/655999-compilers-3-grammars