浅谈 编译器 & 天然语言处理

==============================================java

copyright: KIRA-lznios

==============================================c++

转载请注明出处,这篇是我原创,翻版必究!!!!!!!!!!!!!!!!!!!程序员

==============================================正则表达式

若是以为写个好,请留个言,点个赞。redis

 

最喜欢吴军博士的一句话,和我本人的学习理念比较接近,因此对他的书也很是着迷:技术分为术 和 道,术 是具体作事的算法,道是其背后隐藏的根本机理算法

就像吴军博士说的那样,sql

1.高大上的天然语言处理背后模型机理尽然如此简单(固然细节不简单)数据库

2.怎么像你奶奶解释搜索引擎?其实搜索引擎的背后机理其实简单的不能再简单了,就是布尔运算!!!三句话就能讲明白,一是下载尽量多的网页,二是创建索引,三是根据相关性给网页排序!没了,这就是搜索引擎,任何智能的搜素引擎都逃不出布尔运算的框架。express

3…..

以我我的愚见,首先得深入理解道,而后再去发扬术会比较好。由于只有深入理解道,而后才能举一反十!!!而后在你接触新东西的时候,能对之前学的知识加以联系,发现其中的隐含机理的类似性。并能把一个领域的经典研究方法带到另外一个研究领域。


先交代一下:

1.这是我第一篇,忽然想写点有质量的文章,来和你们分享知识,写的很差的地方欢迎拍砖。

2.本人写过编译器,编译器根本不是什么高大上的东西,本质就是一种数据(信息/语言)处理的方法而已,和处理其余数据同样,并和处理天然语言进行对比

3.下一篇是关于学完编译器以后,应该掌握的技能,即进阶信息安全的基础:

关于一段c/c++代码,编译以后,生成怎么样的x86,calling convention,prolog/epilog,caller-saved/callee-saved register,堆栈平衡,全部变量的内存分布,函数符号修饰成什么样,静态连接,动态连接,地址修正,连接指示对编译过程的影响,如dllimport,dllexport,#pragma,函数声明顺便提一下连接器,以及windows下病毒的运行机理,我不会重点写什么是动态连接,而是解释为何动态连接,及其背后隐藏的缘由

4.下一篇关于OO object model,本人对OO有必定了解,封装,继承(单一,多重,怎么解决菱形多重继承数据二义性问题,微软怎么解决?gcc怎么办?分析咱们用的 prefix算法 实现对象模型的继承 ,并给出拓展),多态,这篇以c++为基本,讲述c++ object model,并给出c++为何转换指针会变化(Base* b = new Derived();编译器怎么理解对象模型的,怎么就能多态??对象模型长成什么样,怎么样会形成覆盖,遮蔽?和多态在对象模型上有什么区别?遮蔽,覆盖为毛就不能多态了?),并分析一下c++对象模型优缺点,容易受到什么攻击(堆溢出,堆喷射),虽然hook 函数指针本质不是c++语言自己形成的。。可是c++对象模型若是对于你们都是好人的状况下,是很优秀的对象模型,but。。。

5.下一篇准备写关于高级(多核)操做系统内核的理解,固然是基于MIT的 xv6 和 Yale的pios ,关于 Vitrual Memory:逻辑地址->线性地址->物理地址, fork/join/ret ,copy-on-write…..

6.再下一篇多是关于 内存数据库 新存储方式的新实现(本人拍脑壳想的),并和 sqlite3,nosql,redis 等内存数据库进行 性能,实现方式 的比较

 

本文参考了数学之美,编译器(虎书 和 龙书),和在USTC老师教的,加上我本身写编译器过程的理解,

最后加上我本身设计的final project:code generation(minijava->x86,AT&T,IA32)

 

本人花了一个学期的时间,认真的写了一个编译器,差很少由如下部分组成:

miniJava compiler ->

implement: lexer,parser,AST,elabrator,garbage collector(Gimple algorithm),

                 code generation(minijava->C), code generation(minijava->java bytecode),

                 code generation(minijava->x86,AT&T,IA32),

                 object model(encapsulation,single inherit,polymorphism)

theory: exception,closure,SSA(static single assignment),

           register allocation(graph coloring)

optimiztion:CFG(liveness analysis,Reaching Definition analysis),DFG,SSA, Lattice, register allocation(graph coloring)

 

写本文的目的:

写完编译器,发现编译器更多的是一种数据处理的方法,而不是什么高大上的东西,我写这篇文章的目的,是想任何读完我文章的人,知道编译器到底在干嘛,编译器到底能干些什么?学了编译器以后有神马好处?学完编译器应该掌握什么技能???

 

我会不断提出问题,引起读者的思考,我喜欢有逻辑的思考问题,但愿这样能让文章更有逻辑性。

并且我写东西,不喜欢记流水帐,好比这个应该怎么怎么样,而是写为何要这样,我喜欢搞清楚其背后的缘由。

本文可能会很长,我会从背后隐含的原理去写,而不去探讨高大上的技术。

好了,废话很少说,正文开始。

 

1. 先说说天然语言处理吧(本人不是很懂),一些基本概念,懂行的人直接跳过,谢谢。

a.首先古老的文明为何会出现文字?

由于文字仅仅是信息的一种载体,意图仍是想把信息记录下来,本质仍是信息,古代没有文字的时候,人们好比到了冬天冷,会发出一些 ,"嗖嗖"的声音,肚子饿了,会发出一些什么什么声音,而后因为声音太多,信息太多,人们没法记住,也没法统一,如此才出现了文字,没有为何,就是由于没有人能记住全部的声音,

这样就须要一种文字,去记录那些信息。

b.有了文字,就必定会有语句,N个文字用不一样的语法规则去拼凑生成的语句,不一样的语法规则,生成不一样的语言,这个很好理解。

c.随着文明的发展,信息愈来愈多,可是文字的数量不能成倍的增加,不然也不便于记忆,这样就出现了聚类,把一些相同概念的意思,概括用一个字(词)去表示,好比一次多义,"日"表示太阳,表示太阳早上从东边升起,从西边落下,因此又能够表示一天,等等。

第c条就是所谓的一词多义,绝对是困扰古今中外语言学者,包括计算机科学家的一个大问题,也就是理解这个词的意思,须要参照上下文(context)

d.常识

The pen is in the box.

The box is in the pen.

第一句正常人都懂,第二句有点坑了,不过外国人很容易理解,因为外国人的常识,经验,因此外国人立马就明白,第二句的pen的意思是围栏。

天然语言处理,想分析语句的语义就又多了一坑。

 

其实我就是想说 c 和 d 是基于 编译器技术的 lexer+parser分析 天然语言的语义 上的一个大坑, 这个就是困扰计算机科学家,语言学家多年,以及阻碍处理天然语言的缘由之一。

 

 e.为何要分词?

像英语这种基本不须要,由于空格就是活生生的分隔符(可是对于手写识别英文,空格不明显,仍是须要分词的),可是对于 中,日,韩,泰 等语言,好比 今天我学会了开汽车,中间没有分隔符,因此须要分词。

分词其实也是一坑,好比:

此地\安能\居住,其人\好不\悲伤

此地安\能居住,其人好\不悲伤

 

2.为何要扯天然语言处理,这个和编译器到底有什么关系?

听我慢慢道来。

天然语言处理,其实就是处理好比,今天\我\学会了\开\汽车。 you \ are \ so \ cool.

而基于编译器技术的 lexer + parser ,则也是同样, 今天\我\学会了\开\汽车,不过一般是处理计算机语言,相似,static void main(string[] args)等等。

 

so:

a.天然语言处理,处理的是天然语言,好比上面举得例子,The box is in the pen. 定义的上下文相关文法,即其中词语的意思不能肯定(一次多义),须要结合相应的语境才能知道pen的意思,和你们作的英文完形填空是差很少的。

b.编译器的如java语法,static void main()定义的是上下文无关文法,注意,上下文无关文法的好处就是,只要你定义的好,不会发生歧义,由于不存在一次多义,稍稍举个小例子。

exp -> NUM

      -> ID

      -> exp + exp

      -> exp * exp

碰见exp就能够无条件分解为后面这四种状况,而后再不断的递归降低(recursive decendent parser/ top-down parsing/predicative parsing )迭代,解析语句。

为何说只要定义的好呢?由于咱们lab用的是ll(k),也就是说,只支持从左到右parser,若是出现左递归就会出现永远循环下去,由于是无条件分解。

定义左递归上下文无关文法坑:

a.左递归->右递归

b.歧义->提取公因式

一些其余编译器应该支持lr(k)

到这里看不懂不要紧,这里只是随便提提。

 

我只是想说,像编译器编译的 c/c++/java...,包括sql语句,都是上下文无关文法,均可以用基于编译器的技术,lexer + parser 去解决问题

 

ok,有的人就要问了,那为何基于编译器的技术,lexer + parser 把天然语言,先分解为一系列的token,以后生成语法树,而后用llk or lrk 去遍历这棵树,而后进行 语义分析, 为何不能很好的处理天然语言?

误区:本来科学家觉得,随着语言学家对天然语言语法的归纳愈来愈完备,计算机计算能力又在逐渐提升,基于编译器的技术应该可以很好的解决天然语言处理。

but:一条很简单的上下文相关的语句,却能分析出很庞大复杂的 AST(parser 返回结果是 语法树), 若是再复杂一点,基于语法树的分析根本行不通。

考虑一句很长的文言文,此处省略100字。

结论:因此说,基于编译器技术的lexer + parser 只适合解决上下文无关文法 定义出的语言。

 

那上下文无关文法 就不能定义 天然语言了??要不试试看?

反正我不试。。缘由以下:

a.想要经过上下文无关文法定义汉语的50%语句,语言学家不只会累死,并且因为一词多义,须要结合语境,因此还要在文法里定义各类语境,能够想象那个工做量  吗

b.定义的上下文无关文法越多,越容易出现歧义(提取公因式),并且会出现左递归(改为右递归),如此,如此,会疯掉的。因此 没法涵盖全部语言语法不说,还有歧义,这个是要作成实际应用的,这样能忍吗?

 

如此说来,20世纪50年代到70年代,用  基于编译器技术 lexer + parser 分析天然语言的语义,绝对是科学家们走的弯路。

直到20世纪70年代,才有先驱从新认识这个问题,基于数学模型和统计,天然语言处理进入第二个阶段。

 

再总结一下结论: 基于编译器技术 lexer + parser 分析语言的语义, 只适合 上下文无关文法, 而上下文无关文法 没法(不容易)定义 天然语言,so,不能用lexer + parser 去分析天然语言的语义。

 

3. 那到底怎么处理天然语言呢?(本文不会详细写怎么处理,只写基本原理),懂行的请自觉跳过,谢谢。

规则统计,用数学的方法去描述语言规律。

注意,统计语言模型的产生初衷是为了解决语音识别问题,也就是说 一句话,让你分析,这句话究竟是不是具备正确意义的天然语言。

用统计的思想思考:一个句子,由特定的单词串组成,s = w1,w2,...,wn ,一个句子有意义的几率是 P(s) ,

由条件几率很容易获得 P(s) = P(w1) * P(w2 | w1) * ..... * P(wn | w1,w2,...,wn-1)

只要算出这个语句有意义的几率,不就能判断到底这句话有木有意义了呢

可是越到后面这个条件几率越难算了,怎么破?

不要紧,马尔可夫为咱们想了一个偷懒并且颇为有效的方法就是,假设 一个词 wi 出现的 几率 只和它前面的那个词 wi-1 有关系,

因此公式就简化为 P(S) = P(w1) * P(w2 | w1) * P(w3 | w2) *  .....  * P(wn | wn-1)

固然,这个模型,不少人第一次见到,确定会问,就这东东,能分析这么难文法的天然语言。。。。吗?

答案是确定的,Google 的 罗塞塔 系统,仅仅开发2年,就是基于相似这种数学统计模型,就一举成名的得到了 NIST 评测的第一。

 

固然,对于高阶一点的语言模型,其余模型,模型的训练,零几率问题,我在本文不想深刻讨论,讨论的重点,主要仍是想放在编译器上面。

 

一点点思考:

说到这里,说一点题外话。本人还写过内存数据库,因此,须要支持sql语句,为sql语句也写过 lexer 和 parser,用的也是上下文无关文法。

考虑若是sql语言,若是发展足够强大,就像天然语言同样,语法愈来愈多,会不会出现 聚类(一词多义) ?若是出现聚类,那根据个人结论,

lexer + paser这种方法不work了,那是否是得用到 天然语言处理的 某些方法,或者其余方法???

因为目前的语言c/c++/java/sql 仍是处于上下文无关文法就能够定义的语言,有个度(界限)的问题,若是跨越到天然语言,则之前的方法根本不能用了,是否是得考虑新的技术。

啧啧,随便说说。

 

4.关于天然语言处理 和 编译器相关技术处理 的浅薄关系 在上面已经说过了,接下来就是我要讲清楚,编译器到底在干什么?

我以前说过,编译器也是对一种语言的处理过程,因此上文和天然语言处理进行了对比,而后引起了一点点小思考。

 

ok,书上说编译器就是把高级语言翻译成低级语言,忘了,书上好像是这么写的。

 

不过我理解的编译器应该是这样,

a. 编译器会通过 lexer + parser + elabraor + code generation : IR(N种)  for optimization + 可能还连接一个garbage collector 

    ->而后生成object file(目标文件),注意目标文件仍是不能运行,可是就差那么一点点,这一点点是什么(对于外部符号,编译器不知道,只能进入符号表,等待连接器来修正)?

    好比你  cl /c main.c  这样只编译不连接,若是出现编译器不认识的符号,不要紧,反正生成目标文件,那些符号就进入了符号表,等连接器下一步工做。

    可是你 cl main.c ,这样既编译又连接,若是有不认识的符号,直接报错(假设你其余目标文件也木有这个符号)

总结:等连接器,把其余的目标文件link到一块儿(主要是地址修正),而后生成可执行文件(静态连接/动态连接/动态连接静态加载/动态连接动态加载,不同), 这样就生成了可执行文件 .exe / a.out ... 芯片上跑去吧

详细细节留给下一篇吧,要写就停不下来。。。

    

b. 编译器确实是把高级语言翻译成低级语言,可是其中会通过不少种IR(中间代码),大部分缘由是由于优化,像gcc就通过N种优化,而后生成一个最简的x86机器码,而后跑在intel的芯片上,固然ARM,MIPS均可以。。。固然你翻译成java的bytecode ,在虚拟机上跑,都是能够的。

IR嘛,举个例子,好比

第一步我就要对AST进行优化,优化一般有 常量折叠,代数简化,标量代替聚量, 常量传播,拷贝传播,死代码删除,公共子表达式删除等等

ne.g.: a = 0+b    ==>  a = b
ne.g.: a = 1*b    ==>  a = b
ne.g.: 2*a          ==>  a + a
ne.g.: 2*a          ==>  a<<1         (strength reduction)
ne.g.: *(&a)       ==>  a
看吧,常量折叠很好理解吧,就是直接把AST给改了,
这里暂时不讨论其余优化。
note:
像a++这种,咱们称之为 "语法糖" 的东西,可能不是在优化器里把改成 a = a + 1; 可能在 parser 里面看到 就直接改了,呼呼。
固然 a += 1,也是赤裸裸的  "语法糖"
第二步,我可能要通过CFG(control flow graph)(SSA在后面讨论)
那就要把IR翻译成三地址码,而后以跳转为分界线,把不跳转的部分组织成块,最后组织成图形结构
第三步,多是DFG优化,利用数据流方程进行优化
第四步,多是活性分析,寄存器分配(图着色)
第五步,多是基于离散数学,格(Lattice)的优化
第N步, 等等 。。。。。。
 
note:小插曲:
编译器优化程序员永不失业理论 : 由于没有一种优化可以老是起好的做用(视具体状况而定),因此任何一种算法都不能把全部程序化简到最简。。。因此。。
但有一种优化总能起好的做用,嘻嘻,那就是寄存器分配(前提是你得有几个寄存器。。。),必定会让你的程序变快。
 
要再也不专门写一篇,讨论编译器优化,我有不少话想写。
 
算了,详细不作讨论,在这里只想说,不少编译器为了进行优化,生成好多好多种IR,在每一层IR上进行不一样的优化,就是为了用数学的方法,去不断简化程序员写的代码,由于不一样的IR对不一样的优化有不一样的功效
 
->目的很明显,就是为了生成最优的机器码
 
 
note:为何编译器要分 lexer + parser + elabraor + code generation 这么多层? 合并几层不行吗?
         答案是固然能够,不过为何这样分,有它的道理,缘由就是模块化,好比,有专门的人研究lexer,好比有Flex等工具,专门的人研究parser。。。但一层一层向下,向上暴露的接口固然是一致的,这样的好处就是,能够专门研究某一层算法,而后直接替换某一层。
 
初窥了什么是编译器,接下来,我想一步一步分析,到底lexer + parser + elabraor + code generation 这么多层,每层编译器在搞什么?
 
a. lexer
 
b. parser
 
c. elabrator
 
d. code generation
 
e. 讨论 exception,closure,SSA(static single assignment) 是怎么样实现的
 
f.  讨论一下garbage collector
 
g. optimization
 
h. 关于咱们学校课程最后的 final project 思考,关于 java 反射机制,gc的世代收集,翻译成 go / js ,jvm ,还有本人写的 minijava 直接 -> x86,我几回推翻重写,不过最后完成了,仍是很 happy (minijava没有很高深的java语法,仅仅是封装,继承,多态,我用x86模拟了而已)
 
a. lexer -> translates the source program into a stream of lexical tokens
 
输入: source program
输出: a stream of lexical tokens
 
 
先举个通俗易懂的例子,好比我要对如下java程序进行 lexer,怎么作 ?

class TreeVisitor{

    public static void main(String[] a){

         System.out.println(new TV().Start());

    }

}

lexer的输出,很明显是,a stream of lexical tokens :class | TreeVisitor | { | publicstatic | void | main | ( | String | [ | ] | a | ) | {System | . | out | . | println | (new | TV | ( | ) | . | Start | ( | ) | ) | ; | } | }

 

 

看一下 Token结构体长成神马样子?

class Token{

public Kind kind; // kind of the token

public String lexeme; // extra lexeme for this token, if any

public Integer lineNum; // on which line of the source file this token appears 目前能够忽略,只是为了输出

 ......}

 

看下输出,你们就明白了:

TOKEN_CLASS: class : at line 5

TOKEN_ID: TreeVisitor : at line 5

TOKEN_LBRACE: <NONE> : at line 5

TOKEN_PUBLIC: public : at line 6

TOKEN_STATIC: static : at line 6

TOKEN_VOID: void : at line 6

TOKEN_MAIN: main : at line 6

TOKEN_LPAREN: <NONE> : at line 6

TOKEN_STRING: String : at line 6

TOKEN_LBRACK: <NONE> : at line 6

TOKEN_RBRACK: <NONE> : at line 6

TOKEN_ID: a : at line 6

TOKEN_RPAREN: <NONE> : at line 6

TOKEN_LBRACE: <NONE> : at line 6

..................

 

ok,分解为了 a stream of lexical tokens ,很明显用一个 队列 去存储它们。

note:

很是建议用队列去存储,为何?

1.咱们lab用的是直接在parser里面一个一个直接读取lexer分解出来的Token,即不能回滚,即上一个Token还得用一个value记录下来,固然你能够

定义回滚几个,而后记录 rollbackToken1,rollbackToken2,rollbackToken3....等

2.用队列虽然浪费了存储空间,可是能够任意回滚任意个数的Token

so,建议看具体须要。

神马状况下会遇到回滚Token?

好比,

MyVisitor v ;

root = new Tree();

因为是递归降低分析(在paser中详细讨论,看完paser再回来理解),只能像微软的编译器同样,写c语言的时候,定义放在语句前面,若是你在中间某个地方写了,int a = fun(1,2);则微软编译器会报错,可是一个这样的小错误,微软的优化器会爆出各类错。。。让你根本就不知道哪错了

回到正题:因为和c语言同样,本编译器算法是,前面是定义,后面是语句。

so,检测到root 的时候 Token是个ID,没问题,可是后面发现Token 是 = 号,也就是你进入 定义和语句的 临界区域了,so,你的代码还在分析定义的代码里,怎么破?你得回滚,而后跳出整个 分析 定义的代码,进入分析语句的代码,而后 current Token 得回滚到 root (原来在=)。

 

note:

可是gcc支持语句中有定义,不是由于 ANSI c 支持,而是gcc进行的拓展。

gcc怎么实现的?其实很容易,和c++/java 同样,加减符号表运算便可

gcc的c还支持bool呢,呼呼。

note:

吐槽微软编译器:

void fun(){}

这样的空函数,微软还不优化,

fun:

    push ebp

    mov ebp,esp 

    push ebx

    push esi

    push edi

这三个是 callee-saved 寄存器,微软还要入栈保存,是否是有点懒了,别说寄存器分配了,若是 寄存器分配(好比图着色) 只用到一个寄存器,这样入栈保存一个不就好了吗?

note:算了,仍是表扬下微软的编译器吧,好比你看到,

   push ebp 

   mov ebp,esp

   push ecx    // 而不是  sub esp,4

为何不用sub esp,4 ? 。。。。。。。这个缘由很深入,由于,一样是往下开辟4个byte, push ecx 用的(指x86,ARM不知道)机器码更少哦

    

 

其实我是想解释,为何 lexer 要 translates the source program into a stream of lexical tokens ?而不分解为其余结构 ?

想一想中文为何要分词? eg,今天我学会了开汽车,你用指针去扫源代码的时候,扫到 unicode  "今" ,你能把它做为一个Token吗?明显不行,由于"今天"才是一个Token。。。那怎么样断句呢?即,怎么分词呢?最简单的方法就是查字典,这种方法最先是由北航的梁南元教授提出的。即,字典里有的词就表示出来,遇到复合词就最长匹配。

可是最长匹配也有问题。

好比, 上海大学城书店,你怎么分?

最长匹配是: 上海大学/城/书店?

显然不对,应该是 上海/大学城/书店

这里不进一步讨论。

 

好了,以前说过,像英文这样 I am so cool. 语句之间有标点符号,语句之中有空格,因此,不须要分词,Token很容易找到!!!!!!

代码也是这样,大部分是有分隔符(以空格分开)的,可是也有例外,好比,

/

//

/*

遇到一个/,你能武断说这个Token是 / 吗?嗯,得看看后面跟的是啥。

 

回到正题,为何要分解为a stream of lexical tokens?

由于好比天然语言是由一个一个单词组成的,单词组成的顺序,则是语法。 

只有先把一个一个单词分解出来,而后去分析每一个单词之间为何这样排列(这就是分析这句话是神马语法 -> 找出它的语法规则 ),而后生成一棵语法树,存储起来。

分词就是lexer干的事情,它的输出就是给 parser 的输入,parser 则负责生成 AST(抽象语法树),并传给 elabrator。

 

note:

说道分词,编译器技术已经完美解决了这个问题(仅仅针对上下文无关文法),即用 正则表达式。 NFA -> DFA

我不想延伸,由于内容太多,之后有机会再写。

 

固然lexer有不少,好比 flex, sml-lex, Ocaml-lex, JLex, C#lex ...... 

 

说道这里,lexer我是否已经讲清楚了呢??我以为差很少了,之后有机会补充。

 

 b. parser -> 根据 递归降低 分析算法,生成语法树 

note:
recursive decendent parser/top-down parsing/predicative parsing  这几个单词是一个算法,都是递归降低分析
 
想了想怎么来讲这个parser,我想我仍是举个实例比较容易理解! 我不喜欢把一个很简单的东西,用不少数学公式去弄的很复杂,我以为作学术,反而应该把复杂的东西,简单化,这样让更多人能看懂其背后的机理其实很easy。
 
先随手写段程序好了, 不要在乎语义上的细节,只是为了说明parser工做机制。

class TreeVisitor{

    public static void main(String[] a){

          System.out.println(new Visitor().Start());

    }

}

class Visitor {

    Tree l ;

    Tree r ;

    public int Strat(Tree n){

    int nti ;

    int a;

    while(n < 10)

        a = 1;

    if (n.GetHas_Right())

        a = 3;

    else

          a = 12 ;

    return a;      

    }

}

递归降低,能够用一个词来来归纳,其实就是 while循环

若是说要返回一个AST,这样固然须要先定义全部抽象语句的类,而后生成其对象,而后reference相互连起来,造成一棵树。

parser 输出返回一棵AST -> theAst = parser.parse();

 

ast.program.T prog = parseProgram();

.......

ast.mainClass.MainClass mainclass = parseMainClass();

java.util.LinkedList<ast.classs.T> classes = parseClassDecls();

...... 

java.util.LinkedList<ast.classs.T> classes = new java.util.LinkedList<ast.classs.T>();

ast.classs.T oneclass = null;

while (current.kind == Kind.TOKEN_CLASS) {

      oneclass = parseClassDecl();

      classes.add(oneclass);

    }

注意,我为何说,递归降低就是while循环,上面漂绿的字体很明显了,当你分析某一种语法的时候,不断用while探测,若是进入下一个语法,则跳出while循环。

再说细一点:

int nti ;

int a;

while(n < 10)

    a = 1;

if (n.GetHas_Right())

    a = 3;

else

    a = 12 ;

函数开始的时候,先分析 "定义" ,分析到 int nti; 没问题,是 "定义" ,而后到 int a; 也没问题,是 "定义"。

可是到了 while 语句,则 编译器代码跳出 分析 “定义” 的代码,进入 分析 "语句" 的代码。

注意一点便可,我上面举得例子。

MyVisitor v ;

root = new Tree();

 

 

OK,返回了AST,好办了,能够直接 pretty print 出来了,由于你已经有了AST,即一棵树,全部这段程序的语义都存储起来了,你想怎么打印,

不就怎么打印了?

好比:

  @Override

  public void visit(ast.stm.If s)

  {

    this.sayln("");//if语句前换个行先

    this.printSpaces();

    this.say("if (");

    s.condition.accept(this);

    this.sayln(")");

    this.indent();

    s.thenn.accept(this);

    this.unIndent();

    this.printSpaces();

    this.sayln("else");

    this.indent();

    s.elsee.accept(this);

    this.unIndent();

    return;

  }

 
note:
这里用的是访问者模式,这里不作讨论。
 
note:
so,对于paser,能够pretty print,因而可知,相似vim / emac 等编辑器,是怎么样智能的处理了? 本人本身想的其中一种方法。
好比文件里有一段code,而后lexer + parser 以后生成了AST,而后修改AST,再把AST打印出来不就好了!!!!固然这个只是其中一种实现方式,
具体vim / emac 怎么实现的,我没看过源码,不知道,我只是给出了一种我本身的想法。
 
parser,就是分析语法(你本身定义的语言的上下文无关文法),而后返回一颗语法树,存储起来,而后传给elabrator。
 
关于parser,我应该说清楚了吧???
 
 
c. elaborator -> 其实就是语义分析,这里被称为  “精细化”  ,其实本质是 type checking,我日常就称之为类型检查。
 
工做职责:好比,看看类型,好比会不会出现相似 string = int + char ?  好比function call会不会调用参数个数是否是多一个,类型和声明的一不同,不然报错,高级一点的elaborator,还会看看哪些变量,声明了,却没有用到,而后报出一个警告,等等。。。。。。。。。。。。。。
 
传统的语义分析方法:
Traditionally, semantics takes the form of natural language specification
 
可是最新的论文,证实能够用数学的方法来完美解决这个技术:
But recent research has revealed that semantics can also be addressed via math, it's rigorous and clean
 
这篇论文的题目是 -> Quick Introduction to Type Systems   ->  type judging
 
举个例子
 
 
上面是假设条件, hypothesis
类型系统是能够计算的, 好比 int + int -> 若是返回 int ,则 juding 正确

理论联系下实际:

 假设定义语义 : int + int -> int

  @Override

  public void visit(ast.exp.Add e)

  {                       

    e.left.accept(this);

    if (!this.type.toString().equals("@int"))

       error("operator '+' left expression must be int type",e.addleftexplineNum);

    ast.type.T leftty = this.type;

    e.right.accept(this);

    if (!this.type.toString().equals("@int"))

        error("operator '+' right expression must be int type" ,e.addrightexplineNum);

    this.type = new ast.type.Int(); //表示当前操做 add,完成以后,“返回”一个操做数类型为 int

    return;

  }

 

 

note:

这里不讨论关于继承(多态),function call等再难一点的语义分析,不是本文重点。

 

OK, elabrator 的工做,总结下,就是先扫一遍AST,而后生成相应的符号表(多态涉及prefix算法计算继承后的对象模型中虚函数表的函数指针排列顺序,这里不讨论),而后进行类型系统的判断,报出一些语句出错的信息,或者警告信息。

 

elabrator我是否已经讲清楚了呢?

 

d. code generation -> 生成 IR

本人作了 minijava -> java bytecode / c / x86

minijava 直接 -> x86,我几回推翻重写,不过最后完成了,仍是很 happy (minijava没有很高深的java语法,仅仅是封装,继承,多态,我用x86模拟了而已)

仍是有必定难度的,用汇编这种低级语言去模拟封装,继承,多态,仍是有必定难度的,放在之后讨论吧,写不完了。

 

e. 讨论 exception,closure,SSA(static single assignment) 是怎么样实现的

 

exception:其实编译器一般有2种方法,

1.基于异常栈

2.基于异常表:pay as you go

细节,不想在本文讨论了。

 

closure: 我会讨论在java非要支持nested function以后,一步一步逃逸变量是怎么样不可以存储,而后引出closure的解决方法的,还会给出closure 和 object model 有什么区别?

 

SSA(static single assignment) 真心是一种牛逼的IR,让不少优化变得很是简单。可是内容太多,写不完了。自从有的这个SSA,gcc版本从某一个版本,忘了,开始所有把基于 CFG , DFG 的优化,变成SSA了

 

f.  讨论一下garbage collector
 
gc实际上要有很大的篇幅去讨论,基本上有这几种,我来数数:
1.基于引用计数的gc(浪费一个int大小要去存引用计数)
2.基于微软的 mark & swap ,mark有个很trick的技巧
3.copy收集 ,咱们lab作的,基于tiger book,13.3节
4.世代收集,关于代数,每代大小的阈值讨论,给予一个可靠的理论分析,本人喜欢用 平摊分析中的,基于动态表(其实就是c++的new)的方法
5.并发的gc
 
仍是须要花大篇幅去讨论,这里不说了。
 
g. optimization
 
优化太多了,真是说不完啊,暂时不想写了。对于优化容易出错,或者直接违反语义的,极端激进的死代码删除等等,老师说,有句名言叫作
1.请不要优化
2.实在想优化,请参考第1条
哈。
 
h. 关于咱们学校课程最后的 final project 思考,关于 java 反射机制,gc的世代收集,翻译成 go / js ,jvm ,还有本人写的 minijava 直接 -> x86,我几回推翻重写,不过最后完成了,仍是很 happy (minijava没有很高深的java语法,仅仅是封装,继承,多态,我用x86模拟了而已)
 
这个final project 实在是写不下,可能要花很长时间,才能把我是怎么想的写出来,先这样吧。。。
 

5. 用编译器知识理解语言小细节

1.好比到底应该写成 char* p; 还应该写成 char *p; 这种问题其实很好理解,为何,编译器怎么处理指针? 即,碰到类型后面碰到*,就把后面的变量当作指针,好理解了吗,这就是为神马 char* p1,p2;  p2不是指针的缘由

我我的喜爱,就把 char* 当作一个类型,只须要注意 char* p1,p2;  p2 这种状况便可,不少人不是喜欢这样写typedef char* pchar吗,这不就是赤裸裸的认为char*就是一个类型吗?没错,我就喜欢把它当作一个类型。

 

2.好比 const 修饰的 变量,老是分不清 ,

 const char* p;

 char const* p;

 char* const p;

 const char* const p;

我说一句话,你就能永远分清,信不信?固然这个是我从effective c++里面学的,

const 出如今*左边修饰的是指针指向的value,而出如今右边则是修饰的是指针,

没错,你已经会了。

前面2个一个意思,都是修饰value是const,第三个是修饰指针是const,第四个是2个都修饰。

 

3.好比神马 前加加,后加加 ,搞不清楚  ++a;a++;....

int a = 1;

printf("%d",a++); 为毛答案仍是1 ?

 

int a = 1;

printf("%d",++a); 为毛答案就是2了 ?

 

你若是学过编译器,你就懂了,你能够这样理解:

printf("%d",a++); 其实会被编译成2句话

printf("%d",a);

a++;(a = a + 1;)

这样,答案是神马,不用我说了吧。这个就叫作后加,懂了吧

printf("%d",++a);实际上也是会被编译成2句话

a++;(a = a + 1;)

printf("%d",a);

为何是2?一目了然,之后还分不清前加后加吗?嘻嘻。

 

4. 其实 循环语句,其实对于x86来讲就1种->跳转,固然跳转有2种,

一是无条件跳转
L:
   jmp L;
二是有条件跳转
L:
  cmp eax,3
  jb L;
随便写的伪代码。
 
固然有的人问了,do while,while ,for 3种循环,你不是打本身脸吗?
 
我能说 其实翻译成 x86就是do while 吗,而 do while 不就是条件跳转吗?
 
while 就是 do while,为何? 由于多一个入口检测而已
for 就是 do while + 入口检测 + update而已。具体神马的,本身去想吧。。。
 

 

 

 

结语:

 

note:

 

    其实编译器技术,还有不少不少,我只是讨论了其中的九牛一毛,并且因为篇幅限制,我写不下太多。做为第一篇文章,暂时先这样吧,之后再更新。

 

 

    本人对信息安全也略懂,因此对底层的一些东西有一些本身的理解,其实这些都是基础,作安全最最重要的基础,是在课本上根本学不到的东西,最最精华的东西,在之后的文章中我会陆续提到:

学完编译器,对语言的理解又更深了一步,好比你看到以下东西,

int c =  4;

int d;

void fun(int a,int b)

{   int n = 4;

     int i;

     for(i=0;i<n;i++)

         printf("a+b=%d\n",a+b);

}

int main()

{

    fun(1,2);

    return 0;

}

要思考,编译以后,生成怎么样的x86,calling convention,prolog/epilog,caller-saved/callee-saved register,堆栈平衡,全部变量的内存分布,函数符号修饰成什么样,静态连接,动态连接,地址修正,连接指示对编译过程的影响,如dllimport,dllexport,#pragma,函数声明等等

之后的文章我会陆续解释。

其实学完编译器的真正效果,就是你看到上面的c,能想到,其实它就是神马。。。

相关文章
相关标签/搜索