本文是本人撰写的编译原理讲义。
本系列讲义适用于:被强迫学习编译原理前端,或者希望弄明白如何做科研的人。
1.括号嵌套序列特例的解决
清晨的第一束阳光终于还是砸了下来。
高斯一晚上尺规作出正十七边形的神迹并未重现。
没辙了,承认失败。
洗了把脸准备回公司,余光瞄到了那个被前女友嫌弃得不行,甚至分手之后退回来的那个鲁班机关盒,不由得一阵苦笑。
唉,还说她一根筋,我看我也多得是处理不了的事。
等一下,瑞格娜处理不了的那个文法是什么来着?S->aSb|a?
如果写成S->{S} | {},不就对应上了前述的合法嵌套的花括号序列!
对啊,为啥这么一个写规则的式子而已,我为什么要限制得那么死非要限定后面只接一个状态呢?明明还可以接更多的状态或者实义字符呀!感觉这玩意儿能做了呀!
兴奋地来到公司,汇报了一下进度,从老板那里争取了更多时间之后,就跟师傅讨论了起来。
然而与他的兴奋形成反差的是,师傅几乎是皱着眉头听完了他的报告,半响之后提出了一个问题:“即使产生规则被你写出来了,但你打算怎么识别这样的序列?DFA并不能识别这样的结构呀!
你看,如果你只是使用DFA,那么在输入一个符号如{之后,状态就会跳转,那么后面的}就无法在DFA中对应上了。”
?没理解,这个状态后面不是可以接一个}括号然后跳转到新的状态吗?
师傅:“那你怎么表示这个嵌套的花括号?你可要知道,你现在不只是在识别{},你可是要识别{{}}的。在我们现在知道说中间的这个{}可以用S表示,那你知道{走一下到S状态,然后再走右括号到达终态,但是你怎么落实中间的这个{}就成了一个大问题了。”
这下终于明白瑞格娜说的,DFA处理不了S->aSb|a的真实含义。
师傅:“所以,如果说你这规则式的升级对应了软件的升级,那么这个用于识别嵌套的硬件,也得跟着升级呐!”
识别嵌套?有那玩意儿咱们不早就做。。等一下,函数不就是可以递归嵌套调用吗?那系统里面用于实现函数递归调用的数据结构就是。。
“栈!”两人几乎是异口同声。
啊太对了!
从师傅那回来,便投入了紧锣密鼓的推理中。
先回顾一下函数的运行过程。
一个函数,往往会先在一段内存空间中放置自己的局部变量。然后一旦调用子函数,就会在当前空间的顶部封上一个临时盖顶,然后以这个顶为底,重新把上部没有用到的空间作为子函数的空间继续使用。
一旦子函数调用完毕,就可以重新打开两个函数之间的那个临时封顶,并且让底部下探到原来函数的底座,这样就把原来函数的内存空间恢复了。
由此,我们实现了函数的递归调用。
在整个套娃调用的过程中,由于内存空间会像是不停堆叠纸箱,又再不停挪开,且遵循后面被调用的子函数将会首先因为返回而被清理的原则,完美对应了基本数据结构,栈。
下一个问题,函数是怎么知道自己何时该进入到下一层,又是何时该返回到上一层的函数?
为了进入下一层函数,比较常见的情况是,调用一个call func_name的指令,其中func_name就是子函数的标识符。
而返回上一层函数,最常用的就是ret指令了。
那如果把左花括号映射成call,把右花括号映射成ret,这不就直接可以使用完全相同的机制从而识别了吗?
1.首先默认系统运行在主函数,层级设为0;
2.读入符号,如果是左花括号,就调用本函数,操作步骤回到1,函数层级+1;如果是右花括号,就调用ret函数,回到上一层函数中调用本函数的下一个步骤,即3。
3.读入符号,如果是右花括号,则调用ret函数,回到上一层函数中调用本函数的下一步,即3;如果不是右花括号,则报错。
错误处理:如果函数层级小于0,则报错,拒绝接受输入串为合法串。
啊爽,又解决了一个特例,接下来就该推广到普遍情况了。(咦,为什么要说又?)
2.句子:被拍扁的语法树
随手写出来一个三层的花括号嵌套{{{}}},盯着另一边的S->{S} | {}文法,陷入了沉思:
这玩意儿结构,怎么那么像洋葱呢?如果把芯一层层推出来,那就得到:
那如果再把文法中的那些S加上?
等一下,这不就是树吗?!
从根节点出发,不停地开枝散叶,最终长成一棵完整的树。
等一下,好像也不是每个点都可以开枝散叶,而只有中间那些之前用来表示状态的节点可以。
也就是说,如果我们所谓组织一个句子,实际上就是根据规则,进行不停地扩展,直到所有的枝丫全部都开出了叶子?
那句子,就是这棵树的所有叶子节点,从左到右的串联??
怎么感觉我刚刚那个从串还原成树的过程,隐隐有点像是英语课上做的句子成分分析。
话说回来,既然这是在做语法分析生成的树,那就叫它语法树吧!
树上面的节点,如果是不可以分叉的叶子节点,既然没有后代了,那就叫它终结符(Terminal symbol)吧,代表的是句子中的实义符号;如果是那些可以分叉的节点,对应的就是句子成分分析中的语法概念,既然可以有后代,就叫它非终结符(Non-terminal symbol)吧!
师傅刚好路过,听到他叽里咕噜在那自言自语,不由得往屏幕上看了看。然而由于站位相反,师傅看到的草图完全倒过来了。
“你这推导挺有意思啊,感情把这树拍扁了就成了句子了?”
…………???…………!!!!!师傅不愧是师傅啊!你要不说拍扁,我都想不到还有这个角度!
师傅被夸得有点不好意思:“这不就是个角度问题嘛,有啥好夸的”
不是师傅,你想啊,我们的这个树如果真要是拍扁了,就好像吃了一张三体里面的二向箔,原来的那些结构信息就全部丢失了,这个时候要恢复就很麻烦了,这不正好就对应上我们现在语法分析器的痛苦嘛!
那为什么我们又可以恢复呢?是因为我们已经有一些之前已经积累下来的知识,或者说是已经提前约好的规则,统称为先验知识。这样就可以像是还原案发现场那样,把句子的结构从一个串重新还原成一棵树!
这个时候困惑的反而是师傅了:“哪来那么多什么先验知识,你这左右花括号不是有挺明显的层次结构吗?”
师傅您看,就拿“我是牛马”这么个句子来看,你要做成分分析,你得知道“牛马”是一个名词,充当宾语,这才好和前面的动词“是”结合为谓语,接下来才能构成出主谓结构,进而生成一棵完整的树。
重点在于,你说整个语法都写成像花括号序列这样有明确结构信息的,那恢复起来当然容易,但是对于给人用的语法,我觉得极大可能会为了方便性,并不会在句子中留下那么多有助于恢复的结构化信息,这个时候先验知识的作用就能体现了。
比如说上面说的,牛马,就需要查询字典,才知道它是一个名词,而不是独立的两个字,再结合它的位置信息,可以进一步判定为宾语。
“说得很好”师傅由衷点点头,“没想到这些文科的知识居然也会用在我们这些理科生手上。”
师傅你还真别说,我现在做着做着,感觉文理的分界线在这个项目里面都模糊了。谁又说语法这种事是文科生的专利呢。
3.一个幽灵,取舍的幽灵,在工程中游荡
翌日,会议室,向老板对当前的成果进行了汇报。
“所以,正则文法由于只能在后面接一个状态,限制太大,导致无法识别嵌套,需要解除这个限制,这样我们就能在一条产生式的左边随便写是吧。。
等一下,我感觉你这个语法树有个问题啊。编程语言中,有好些东西都是上下文有关的,包括变量要先定义才能用;goto语句后面得接上一个提前定义好的标号;break 和continue这种语句只能出现在特定的上下文里面的。
你刚刚介绍说,用户的编程语言可以用S->S_1;S_2的方式从而串联多个编程语句(下标用来区分两个不同的S,没有实义)。但如果我在前一个S_1语句里面定义了变量,你是能解析这句定义语句了,但这个信息要怎么传递到后面一个S_2呢?
同样,以break语句为例,如果它出现在语法树一个很末端的位置,要怎么把是否在循环里面的信息传递到里面呢?
break出现在循环以外,那肯定不是我们想要的语言,这种当然应该也属于语法分析的范畴呐!
是不是应该继续升级一下你们的文法,让它可以处理上下文关系?真要搞出来我觉得就叫上下文有关文法就好。你们现在这个充其量只能算是上下文无关文法,根本处理不了上下文。你看那个什么非终结符,它的展开根本就和上下文没有关系,完全就是随地大小变嘛!”
老板这个接地气的比喻,算是缓和了一下前面质疑带来的低气压,会议室里充满了快活的空气,大家的脑子都活跃了很多。热热闹闹讨论了半小时,但进展寥寥。
休会期间,和师傅在厕所碰头了。
师傅:“我想起之前看过的一个故事,说是美国一个工厂和中国一个工厂都引入了一条肥皂生产线,这个生产线有一定概率会产生空盒子。美国工厂花费巨大,结果搞出了什么X光和机械臂搞定了这个问题。而中国的工厂,由于缺乏资金、技术,只能搬来一个大风扇,直接吹走那些空盒子。感觉咱们现在工期这么赶,是不是也可以用些蠢方法,先把工程做完再说,以后再来研究那个什么上下文有关文法。”
一个关于有缺陷生产线的商业寓言
两人商议了一下,感觉有头绪了,回到会议室开始汇报他们的计划。
“对于您刚刚提到的,关于标识符的定义,咱们可以预先定义一个符号表,这样在前面扫描出来的注册过的标识符,咱们就把它注册到符号表里面;在后面如果要用到标识符,就在符号表上面先查询,只有确定存在,才允许使用。
对于break和continue的情况,只要在构建语法树之后,针对这两个特殊情况进行检查,看他们是否存在于一棵循环的子树中即可。“
老板想了好一阵,终于是点了点头,做了一个总结:“你们前面不是说为了要避免特事特办所以才搞的正则文法嘛。但看来,特事特办这个幽灵现在终究还是又杀回来了嘛。
只能说,上下文有关文法可能是一个很好的理论,但离我们太遥远了。在工程成本和完美理论之间,可能还是要进行取舍的。
那先这样吧,接下来就等着你们的研究成果了。散会!”