我的BLOG之旅--LET'S BUILD A COMPILER!(1简介)

                          LET'S BUILD A COMPILER!
                                一起做编译器
                                       By

                            Jack W. Crenshaw, Ph.D.

                                  24 July 1988


                            Part I: INTRODUCTION
                               第一章:简          介


*****************************************************************
*                                                               *
*                        COPYRIGHT NOTICE                       *
*                                                               *
*   Copyright (C) 1988 Jack W. Crenshaw. All rights reserved.   *
*                                                               *
*****************************************************************
译者:问风 Email:wenfengmtd@163.com

简介

 这些一系列文章是有关开发语言语法分析器和编译器的理论和实践资料.在我们完成这个项目之前,我们将隐藏所有编译器构造方面知识去设计一个新编程语言和一个可以运行的编译器.

虽然我不是一个计算机科学家(我的博士学位是与之不同领域的物理学)可是这
么多年我却一直对编译器很感兴趣.在写这篇文章之前,我已经购买和尝试消化
每一个书与之相关主题的内容.我不介意地告诉你这是一个十分缓慢的过程.因为有关编译器的文章是写给计算机科学专业的对于不是此专业的我们来说读起来十分费劲.但是通过这几年的学习,这些理论已经慢慢被我接受.这正是我决定开始分身去研究计算机方面现在我打算与你分享我所学的东西.当然在你学习完这一系列知识之后并不意味着你就是一个计算机科学家也并不代表你完全懂得了所用深奥的计算机理论.在这里,我打算完全忽略所有编译这一主题方面的所有理论.你所将知道(学到)的是所有建立可运行系统所需要的实践方面的知识.

 这是一个"通过动手学习"的丛书.在这个系列课程中我将演示在一台电脑中上所做的实验.你最好随着步骤,重复我所做的实验并进行一些你自己的实验.
我将使用在一个PC机兼容的编程工具Turbo Pascal 4.0.(译者用的是Turbo Pascal 7.0,也许使用dephi也可以,不过没有试过).我会定时地插入用TP写的例子.这些都是一些可执行的代码,因此你可以直接复制到你的电脑并运行.(注:译者的机上也可以通过)如果你没有Turbo,对接着来所做的一切你将会被严重受限如果你没有,我强烈你安装一个.毕竟,这是一个优秀的产品,比其它产品还要好!(看些译者并不认为,可是当深入学习后发现确如作者所说那么优秀,其简洁代码及易学的语法吸引着我)

 有一些有关编译器方面文章向你展示许多例子,或向你展示一个完成的产物(如Samll-C)你可以复制和使用这个成品,并不要求全明白它是如何运作的.我期望比其做得更多.我希望教你"如何"把这些完成,以至能自己进行且能扩充它,而不单单是复制我所做的一切.

 这诚然是一个有志向的想法,因此,它将不会用一个文档完成.我打算通过一系列文章这样一个过程去完成它.每篇文章将包含编译器理论的一个方面,且几乎是孤立的.如果你只是对某一方面感兴趣,那么只要看一个相关的文章就可.当完成一篇文章时它都会被上传,因此,在你认为你已读完先前文章,你将不得不等待最新一个文章.请要有耐心.(事实上如果是一位Jack W. Crenshaw的忠实读者,那么他可能不得不由1988年等到1995年,后面2006年也出了第17章)

 

一般编译器理论文章包含许多背景知识我们这里不汲及的.通常次序如下:
(1)一个介绍性章节描述什么是一个编译器
(2)一个或二个章节描述有关使用BNF描述语法方程式,
(3)一个章节或二个章节有关词法扫瞄,通过强调确定型和非确定型的有限自动机
(4)几个有关语法分析理论章节,从开始讲述自顶向下的递归下降分析器到以
LALR语法分析器结束
(5)一个有关中间代码生成章节,着重P-code和类似的逆波兰表示法许多有关子程序,参数传递和类型检测的可选方法
(6)一个章节有关代码生成,通常基于一些具有简单指令集的虚拟CPU.许多读者
(事实上包括许多大学课堂)不会汲及此内容.
(7)一个或两个结束章节有关代码优化.这章节通常也是很难读的.

 在以上列举的顺序,我将采取非常不同的步骤.首先,我将不会详述太多有关选项(编译选项).我将会给你一个有效的方法如果你想探究选项,非常好...我鼓励你自己去完成...但我坚持我所认为的.我也将跳过所有会让人们睡觉的理论.不要给我误导:我并不是轻视这些理论,当遇到一个所给语言中非常棘手的部分时这些理论是极其重要的.但我认为把重要的事情放在第一位.这里我们将汲及到不需要理论支持占95%的编译器构造技术.


 我也将讨论一种有关语法分析的方法:自顶向下的,递归下降语法分析.这是唯一
一个可以用手工完成的编译器的技术另一个方法是只有你有类似YACC工具时
才有用的方法,当然,使用YACC方法也不用我们关心最终产品将占多大内存
(因为是自动生成)
 我也截取了一页Small C之父Ron Cain的研究.不同于许多编译器作者曾采用中间代码,如P-code把编译器分成两部分(前端以生成P-code结束,而后端以把P-code转换生成可执行代码结束),Ron 展示了一种如何以汇编语言语句的形式把让一个编译器生成可执行代码这种代码将不是世界上最紧凑的代码...生成代化代码是一件十分困难的工作.但是它可以运行,且运行相当好.正是如此,我才不至于给你留下我们最终产品是没有价值的印象.
我打算向你展示如何通过一些优化让编译器"加大马力".


 最后,我发现了让我不用费力阅读冗长艰深的代码示例,而能继续开发的是最有效的诀窍. 这些技巧里最主要的是在最初的设计工作中采用单个字符记号,而不嵌入空格.(第二章就有讲)我考虑如果我能写一个语法分析器去识别和处理I-T-L,那么我也能让它去在识别同样的IF-THEN-ELSE而我实现了.在第二节"课",我将向你展示你可以如何简单的扩展一个简单的词法分析器使之可以处理任意长的记号作为另一个技巧,我完全忽略了文件I/O,考虑到如果我能从键盘读源代码和输出对象到屏幕,那么我也能把其做成从文件输入到文件输出经验证明只要一个翻译程序可以正确运行,那么一个直接可完成的事情就是重定向I/O到文件.(也就是说输出到文件只要简单重定向就可以了,它不是重点)最后一个技巧是我没有打算去做错误校正和恢复.我们编写和程序如果会编译出错,那并不会崩溃,它会简单的停在第一个错误...就像良好的ol' Turbo所做一样.还将有其它技巧你所将看到随着你深入进去.它们中许多并不能在任何编译原理的书中出现,但它们有用.(译者注:如第二章的优化一小节就提出了作者的观点,但现在的教科书也能找到相一致的技巧,这也说明技术是在进步的)

 有关风格和效率的单词.就如你将所见,我倾向把程序写成非常简短和易明的程序块.我们写的过程中没有会超过15至20行长的.我是一个KISS(Keep It Simple, Sidney,注:由于便于读者明白什么是KISS,这里就不翻译了)学派的热衷者.我永远不会把问题复杂化.这样会让效率低?可能,但你将不喜欢这样的结果.就如Brian Kernighan曾说过的,开始让它可以运行,然后再让它运行得快.如果稍后你想回来增强我们写的过程代码,你将可以完成,因为这些代码将是可读且易于理解的.如果你真的想这样做,我还是鼓励你直到程序已经完整时你才把成品改成你所想的那样.


 我也一个倾向去延迟构建一个模块直到我发现我需要它.如果尝试预期所有的可能性,那么将来的随机性将会让你发疯,而且你会认为错误无处不在在现在屏幕编辑程序和快速编译器下,我不愿去改变一个模块.除非当我感到需要一个更强大时我才写一个我所需要的最后给一个的忠告:其中一个原则是我们将在这坚持不采用P-code或虚拟CPU.而是着手于一旦产品可以运行,那么可执行代码至少是以汇编语言的源代码.然而,你可能不喜欢我所选择汇编语言.它将会是68000CPU的汇编代码,那怎么能在我的系统(在 SK*DOS下)运行?如80x86的CPU将会更加明显,尽管我不想把这作为一个问题.事实上我想只要'86汇编语言就可以写出其等价的代码.

THE CRADLE
(注:起初译者将其译为摇篮或发源地,但总觉别扭,既然其为主程序模版的名,这里就索性不译了)

 每个程序需要一些模板...I/O例程,错误信息例程等.这些程序在我们开发的不同编译程序时将没有差别.我已经尝试把这些例程(函数,过程)尽可能的抽象出来而不遗漏.以下给出我们所需最小的模版代码.它包含一些I/O过程,一个错误处理过程,一个框架,.和一个空的主函数.我称他们为cradle.当我们可以开发另外的函数,过程时,我们将把它们加入cradle,并在我们需要地方加入调用.做一个cradle的副本并保存它们(就是在你的机上复制一份,并命名为cradle,下面有提供源码),因为我们将不止一次的用到它们.


 这里有许多不同的方法组织一个分析程序的扫瞄活动.在Unix系统中,作者倾向于使用get 和ungetc.我已经非常幸运可以把方法在这里展示,就是用一个单个,全局的,先行字符初始程序的一部分(目前为止,只有一个部分)用以"一个一个地"读入输入流中第一个字符在Turbo 4.0不需要其他特殊的技术...每一个连续读入的调用GetChar(库函数)将读出流中下一个字符


{--------------------------------------------------------------}
program Cradle;

{--------------------------------------------------------------}
{ Constant Declarations }

const TAB = ^I;

{--------------------------------------------------------------}
{ Variable Declarations }

var Look: char;              { Lookahead Character }
                             
{--------------------------------------------------------------}
{ Read New Character From Input Stream }

procedure GetChar;
begin
   Read(Look);
end;

{--------------------------------------------------------------}
{ Report an Error }

procedure Error(s: string);
begin
   WriteLn;
   WriteLn(^G, 'Error: ', s, '.');
end;


{--------------------------------------------------------------}
{ Report Error and Halt }

procedure Abort(s: string);
begin
   Error(s);
   Halt;
end;


{--------------------------------------------------------------}
{ Report What Was Expected }

procedure Expected(s: string);
begin
   Abort(s + ' Expected');
end;

{--------------------------------------------------------------}
{ Match a Specific Input Character }

procedure Match(x: char);
begin
   if Look = x then GetChar
   else Expected('''' + x + '''');
end;


{--------------------------------------------------------------}
{ Recognize an Alpha Character }

function IsAlpha(c: char): boolean;
begin
   IsAlpha := upcase(c) in ['A'..'Z'];
end;
                             

{--------------------------------------------------------------}

{ Recognize a Decimal Digit }

function IsDigit(c: char): boolean;
begin
   IsDigit := c in ['0'..'9'];
end;


{--------------------------------------------------------------}
{ Get an Identifier }

function GetName: char;
begin
   if not IsAlpha(Look) then Expected('Name');
   GetName := UpCase(Look);
   GetChar;
end;


{--------------------------------------------------------------}
{ Get a Number }

function GetNum: char;
begin
   if not IsDigit(Look) then Expected('Integer');
   GetNum := Look;
   GetChar;
end;


{--------------------------------------------------------------}
{ Output a String with Tab }

procedure Emit(s: string);
begin
   Write(TAB, s);
end;

 


{--------------------------------------------------------------}
{ Output a String with Tab and CRLF }

procedure EmitLn(s: string);
begin
   Emit(s);
   WriteLn;
end;

{--------------------------------------------------------------}
{ Initialize }

procedure Init;
begin
   GetChar;
end;


{--------------------------------------------------------------}
{ Main Program }

begin
   Init;
end.
{--------------------------------------------------------------}

 这就是入门介绍.复制以上代码到TP并编译它.保证它编译和运行正确.然后着手第一讲,表达式分析.

*****************************************************************
*                                                               *
*                        COPYRIGHT NOTICE                    *
*                                                               *
*   Copyright (C) 1988 Jack W. Crenshaw. All rights reserved.             *
*                                                               *
*****************************************************************
谢谢你的阅读,有不足之处请指正!
Email: wenfengmtd@163.com
工作室:爱克斯菲索芙特(XFREESOFE)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值