我的BLOG之旅--LET'S BUILD A COMPILER!(3更多表达式)

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

                     Jack W. Crenshaw, Ph.D.

                            4 Aug 1988


                    Part III: MORE EXPRESSIONS
       更多的表达式
*****************************************************************
*                                                               *
*                        COPYRIGHT NOTICE                       *
*                                                               *
*   Copyright (C) 1988 Jack W. Crenshaw. All rights reserved.   *
*                                                               *
*****************************************************************
译者:问风 Email:wenfengmtd@163.com

简介

在上一部分,我们分析了用于一般数学表达式的语法分析和翻译技术.我们以一个可以处理满足以下两个约束的任意复杂表达式的小型语法分析器来结束上一章节,
(1)只有数学因式,没有变量
(2)数学因式限制为单个数字

在这一章节,我们将除去以上约束.我们将扩展我们已做的一切,包括赋值语句和函数调用.记住,虽然第二个约束是我们自己定的--一个让我们更方便,更容易设计,更能集中基本原理的约束.就如你接下去所见的,这个约束是很容易删除的,所以不要太过担心它.我们使用这个技术是为了我们服务,请你相信当我们做好准备时就能把约束去掉.

变量

在实际中,我们经常看到许多含有变量的表达式,例如:
               b * b + 4 * a * c

难以想像不能处理含有变量表达式的语法分析器会有多好.幸运地是,这很容易实现的.


请回想我们当前的语法分析器,它允许有两种因式:常整数和具有圆括弧的表达式.用BNF记号表述如下:

     <factor> ::= <number> | (<expression>)

这里,'|'代表'or'(或),意味着对于factor(因式)两种形式的任一种形式都是合法的.应该也记得,对于识别这两种不同形式我们并没有困难.先行字符判断'('为一种情形,而一个数字则属于另一种情形.

 大概你不会再吃惊,一个变量也是另一种形式的因式.所以我们扩展上面的BNF如下:        

     <factor> ::= <number> | (<expression>) | <variable>

同样,这样不会产生二义性:如果先行字符是一个字母,我们就可知接下来的是一个变量;如果是一个数字,我们得到的是一个数字.当我们翻译一个数时,我们就生成一条LOAD(装入)这个数的代码,就如把一个立即数送入D0.现在我们也是一样,只是装入的是一个变量.

一个在代码生成中兼有的复杂性起源于这样一个事实:大多数68000操作系统,包括我所用的SK*DOS都要求把代码写成"position-independent"(位置独立)形式,这意味着所有一切都是PC相关的.
装入一个变量的汇编语言形式如下:
               MOVE X(PC),D0

这里X当然是一个变量名.为了增加语法分析器分析变量表达式的能力,让我们把当前版本的Factor函数改为:

{---------------------------------------------------------------}
{ Parse and Translate a Math Factor }

procedure Expression; Forward;

procedure Factor;
begin
   if Look = '(' then begin
      Match('(');
      Expression;
      Match(')');
      end
   else if IsAlpha(Look) then
      EmitLn('MOVE ' + GetName + '(PC),D0')
   else
      EmitLn('MOVE #' + GetNum + ',D0');
end;
{--------------------------------------------------------------}

我在前面也讲过扩展语法分析器是多么容易的一件事,因为方法具有固定结构的.你可以看到在这里同样适用.这次它花费总共只有2行额外代码.也应注意, if-else-else结构是如何精确地表述BNF的语法方程的.

好,编译和测试这个新版本的编译器.应该不会有太大的错误,对吧?
                            
函数


这里还有一种许多编程语言支持的常见因式类型:函数调用.对于我们来说要处理好函数问题现在还为时过早,因我们还不能处理参数传递问题.甚至,一个"真实"的语言包含着支持超过一种类型的机制,其中一种类型就是函数类型.我们也还不能处理这个问题.但出于以下两个理由,我仍想现在就实现函数:首先,它可以让我们概括语法分析程序,在某些方面与最终的语法分析程序形式很相近,第二,它也引出了一个新的十分有价值去讨论的问题.

直到现在,我们已经有能力写一个称为""predictive parser."(预示语法分析程序)的程序.这就是说,无论在任何一点上,我们都能根据先行字符来正确的知道接下来要做什么.(译:就是先行预测技术)但是当我们加入函数后,它就不适用了.因为每种语言都有其命名规则来构造一个合法的标识符.现在,我们简单把标识符规定了一个字母'a'...'z'.问题就在于一个变量名和一个函数名有着相同的命名规则.那么我们怎样区分是标识符还是函数呢?一种方法是在他们使用之前都要先声明.Pascal语言采用的就是这种方法,另一种方法是我们可以要求一个函数后跟一个(也许是空)的参数列表.而这种规则被C语言采用.

因为我们设计中至今没有一个声明类型的机制,所以我们采用C的规则.由于我们也没有处理参数的机制,我们只能处理空参数列表的函数,因此函数调用将有已下形式:
                    x()  .
因为我们不处理参数,所有什么也不用做,除了调用函数,我们所要做的是用一个BSR(子程序调用)命令来取代一个MOVE

既然在Factor过程测试,当先行字符是一个字母时存在着两个可能分支,所有我们把其分开独立两个过程.修改Factor过程如下:

{---------------------------------------------------------------}
{ Parse and Translate a Math Factor }

procedure Expression; Forward;

procedure Factor;
begin
   if Look = '(' then begin
      Match('(');
      Expression;
      Match(')');
      end
   else if IsAlpha(Look) then
      Ident
   else
      EmitLn('MOVE #' + GetNum + ',D0');
end;
{--------------------------------------------------------------}

并在Factor过程前插入一个新的过程:Ident

{---------------------------------------------------------------}
{ Parse and Translate an Identifier }

procedure Ident;
var Name: char;
begin
   Name := GetName;
   if Look = '(' then begin
      Match('(');
      Match(')');
      EmitLn('BSR ' + Name);
      end
   else
      EmitLn('MOVE ' + Name + '(PC),D0')
end;
{---------------------------------------------------------------}

好,编译的测试这个版本.它能分析所有合法的表达式吗?它能正确地标志一个错误的形式吗?

我们应注意最重要的一点是即使我们不再有一个预示语法分析程序,对于我们采用的递归下降方法也不会增添任何复杂性.这样,当Factor过程发现一个标识符(字母),它也不知道它是一个变量名还是一个函数名,这并不是它所真正关心的.Factor过程只是简单地把这个问题传给Ident过程,并让它去断定.过程Ident则依次读入标识符,并读多一个字符去决定它现在处理的标识符是哪种类型.


紧记这个方法.这是一个非常有用的概念,而且无论什么时候当你遇到二义性情形要求先行扫瞄时,它都应该被采用即使你不得不要先行扫瞄几个记号,这个原理就可以适用.


更多有关错误处理


当我们在谈论基本原理时,这里还有另一个重要的问题应指出:错误处理.注意到虽然我们做的语法分析器可以正确地拒绝(译:almost,几乎,下面会有解释为什么用almost)每一个我们送给它的畸形表达式,并有一个有意义的出错信息,我们本不用做太多工作让其发生.事实上,整个语法分析程序本质上(由Ident到Expression)只有两个有关错误程序调用.甚至这些都是不必要的...如果你再看看Term 和 Expression代码,你会发现这些相关的语句都是不可达的.我把它们放入只是早期出于保险考虑,但现在它们不再需要.为什么你现在不删除它们呢?

那么我们如何更自由地获得好的错误处理呢?这很简单,我已经小心地避免直接用函数GetChar读一个字符.取代直接使用GetChar,在错误处理上我依靠GetName,GetNum,和Match去为我完成错误检测.仔细的读者也应该注意到一些Match调用(例如,在Add和Subtract中)其实是不需要的.因为我们已经知道当我们在哪里得到的字符会是什么字符...但是让它们留在那里会让结构更为对称,而且一般用Match代替GetChar是一个好的设计规则.

我在上面用了一个"almost".有一种情形是我们错误处理想解决的.迄今为止,我们还没有让我们编译器知道一行结束的特征是什么,也没有告诉当嵌入空格时编译器该如何做.所以一个空白符(或其它不属于可识别字符集的其它字符)都会使我们的编译器忽略还没识别的字符而终止,在这一点上它也许可以被证明是一个合理的行为.但是在一个真正的编译器中,通常有另一个语句跟在一个可以工作的语句后,以至任何一个不认为是我们表达式一部分的字符将被使用或是被拒绝为下个表达式.


但它仍然是非常简单的修改,即使它只是一个临时的.我们不得不断言表达式应该以行结束符而结束,例如,一个回车为了了解我正在讨论的,尝试输入一行:

               1+2 <space> 3+4

看是如何把空格看成一个终结符的?现在,为了让编译器可以适当地标记,在主函数Main中,仅在Expression调用后加入一行:
               if Look <> CR then Expected('Newline');
它可以捕捉留在输入流中的一切.不要忘记增加一个常数语句定义CR:
               CR = ^M;
和以住一样,重编译程序并验证它可以做它所能支持的.


赋值语句

好,我们已经有一个可以工作得非常好的编译器了。我想指出的是,不包括cradle我们只用了88行可执行代码。但编译的对象文件异常大,占4752字节.但这并不坏,想想我们并不难保存这些源代码和对象文件.我们仅坚持KISS原则

当然,分析一个表达式之后如果不进行处理它,这并不是太好.表达式通常(但不是总是)出现在赋值语句中,如下形式

          <Ident> = <Expression>
其实,我们离可以有能力分析一个赋值语句只有一瞬之差,所以让我们把这最后一步完成.仅仅在过程Expression之后加入如下新的过程:

{--------------------------------------------------------------}
{ Parse and Translate an Assignment Statement }

procedure Assignment;
var Name: char;
begin
   Name := GetName;
   Match('=');
   Expression;
   EmitLn('LEA ' + Name + '(PC),A0');
   EmitLn('MOVE D0,(A0)')
end;
{--------------------------------------------------------------}

再一次留意到,代码正好与BNF一致.进一步可留意到错误检测并不难,全交由GetName和Match完成

出于要求构造PC相关的代码,两行汇编译代码不得不在68000中特殊处理.

现在只要在主函数main中把Expression调用改为Assignment调用.如此而已.

讨厌的工作!实际上我们正在编译赋值语句.如果只用一个语言只用这一种类型的语句,那么我们就可以把它放入一个循环中而且我们也就有一个完全的编译器了

当然,一个语言不可能只有一个类型的语句.还应有一些如控制语句(条件语句和循环语句),过程,声明等等.但令人振奋的是,我们已经处理的算术表达式是一个语言中最有挑战性的.相对我们已经做的,控制语句将是十分容易的.我将会把它们补充在第15章节.而其它语句也将同步完成,只要我们记住KISS原则.


多字符记号


贯穿整部书,我已经很小心限制我们所做一切都为单字符记号,并一直让你确信把其扩展成多字符记号是不太困难的.我不清楚你是否相信我...如果你过去曾有一点怀疑,我真的不想责备你..在接下来的章节里我会继续用这方法,因为它帮助我们避开了复杂性.但我乐意补充这些断言,通过展示你是多么容易地真正扩展它来总结语法分析器的这一部分内容.在这当中,我们也将为嵌入空白符作准备.在你接下来改动代码之前,虽然只有一小部分改动,请用另一个文件名来保存当前版本的语法分析程序.我们会在后面的部分多次它,且我们也将在单字符记号版本下开发.

许多编译器把处理输入流分成一个独立的模块称为词法分析程序.其主要思想是词法分析器处理一个接一个的字符输入,并返回一个在流中的分离单元(记号).当我们想这样处理时,可以实现它,但我们现在并不需要.我们只需要对GetName和GetNum进行很小的局部修改就可以使其处理多字符记号

一个标识符通常定义为开头字符是一个字母,而余下为字母数字式的串(字母或数字).为了完成它,我们需要另一个识加函数:

{--------------------------------------------------------------}
{ Recognize an Alphanumeric }

function IsAlNum(c: char): boolean;
begin
   IsAlNum := IsAlpha(c) or IsDigit(c);
end;
{--------------------------------------------------------------}

把上函数加入到你的语法分析程序中.我把它放在IsDigit之后.当你实现时,最好也把它作为Cradle永久的一员(译:就是作为模版的一部分)

现在我们需要修改函数GetName的返回值一字符代替为一字符串:

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

function GetName: string;
var Token: string;
begin
   Token := '';
   if not IsAlpha(Look) then Expected('Name');
   while IsAlNum(Look) do begin
      Token := Token + UpCase(Look);
      GetChar;
   end;
   GetName := Token;
end;
{--------------------------------------------------------------}

简单地,把GetNum修改为:

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

function GetNum: string;
var Value: string;
begin
   Value := '';
   if not IsDigit(Look) then Expected('Integer');
   while IsDigit(Look) do begin
      Value := Value + Look;
      GetChar;
   end;
   GetNum := Value;
end;
{--------------------------------------------------------------}

令人惊讶的是这就是语法分析程序实质上需要改动的全部地方.在过程Ident和Assignment的局部变量Name原来声明为字符类型,现在必须声明为string[8](显然,我们可以选择让字符串长度更长,但许多汇编程序在某种程度上都限制了长度.完成这些改动,并重编译和测试.现在你相信这是一个简单的改动了吧?

空白符


在我们暂时抛开这个语法分析器之前,让我们看看空白符问题.就现在的情况来看,语法分析器将的(或是简单的终止)在一个嵌入在输入流中任意位置上的空白符.这是一个相当不友好的行为.所以让我们进一步开发以消除以上的限制.

使处理空白符容易的关键就在于提出一个简单的规则来规定语法分析器应该如何对待输入流,并能使得这个规则在任何地方都可以执行.直到现在,因为空白符是不允许的,我们就可以假定在每个语法分析行为之后,先行字符Look都包含着下一个有意义的字符,所以我们可以立即对Look进行测试.我们的设计是基于这个原则的.

对于我来说它仍为一个好的原则,所以它也是我们以后将延用的规则.这意味着所有先行预测输入流的例程必须跳过所有的空白符,并把下一个非空白符保存在Look中.幸运的是,我们已经小心地采用GetName, GetNum, 和 Match来处理大部分的输入.这里仅三个例程序(加上Init)需要我们修改

不会惊讶,我们仍以一个新识别例程开始修改:

{--------------------------------------------------------------}
{ Recognize White Space }

function IsWhite(c: char): boolean;
begin
   IsWhite := c in [' ', TAB];
end;
{--------------------------------------------------------------}

我们也需要一个例程去消耗空白字符,直到找到一个非空白字符:


{--------------------------------------------------------------}
{ Skip Over Leading White Space }

procedure SkipWhite;
begin
   while IsWhite(Look) do
      GetChar;
end;
{--------------------------------------------------------------}

现在,在Match,  GetName,  和  GetNum中加入对 SkipWhite的调用


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

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


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

function GetName: string;
var Token: string;
begin
   Token := '';
   if not IsAlpha(Look) then Expected('Name');
   while IsAlNum(Look) do begin
      Token := Token + UpCase(Look);
      GetChar;
   end;
   GetName := Token;
   SkipWhite;
end;


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

function GetNum: string;
var Value: string;
begin
   Value := '';
   if not IsDigit(Look) then Expected('Integer');
   while IsDigit(Look) do begin
      Value := Value + Look;
      GetChar;
   end;
   GetNum := Value;
   SkipWhite;
end;
{--------------------------------------------------------------}
(注意,这里我重新编排了一下Match的语句顺序,但没用改变其功能)

最后,我们在Init需要跳过所以空白字符(译:"最初的泵"--泵去空白符)

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

procedure Init;
begin
   GetChar;
   SkipWhite;
end;
{--------------------------------------------------------------}

完成以上改动并重编译程序.你将发现为了避免Pascal编译器的出错信息,你将不得不把Match移到SkipWhite之后.和以往那样测试程序保证它正常工作


因为在这小节中我们已经做了许多改动,我重现整个语法分析程序如下:

{--------------------------------------------------------------}
program parse;

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

const TAB = ^I;
       CR = ^M;

{--------------------------------------------------------------}
{ 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;


{--------------------------------------------------------------}
{ 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;


{--------------------------------------------------------------}
{ Recognize an Alphanumeric }

function IsAlNum(c: char): boolean;
begin
   IsAlNum := IsAlpha(c) or IsDigit(c);
end;


{--------------------------------------------------------------}
{ Recognize an Addop }

function IsAddop(c: char): boolean;
begin
   IsAddop := c in ['+', '-'];
end;


{--------------------------------------------------------------}
{ Recognize White Space }
                            
function IsWhite(c: char): boolean;
begin
   IsWhite := c in [' ', TAB];
end;


{--------------------------------------------------------------}
{ Skip Over Leading White Space }

procedure SkipWhite;
begin
   while IsWhite(Look) do
      GetChar;
end;


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

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


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

function GetName: string;
var Token: string;
begin
   Token := '';
   if not IsAlpha(Look) then Expected('Name');
   while IsAlNum(Look) do begin
      Token := Token + UpCase(Look);
      GetChar;
   end;
   GetName := Token;
   SkipWhite;
end;


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

function GetNum: string;
var Value: string;
begin
   Value := '';
   if not IsDigit(Look) then Expected('Integer');
   while IsDigit(Look) do begin
      Value := Value + Look;
      GetChar;
   end;
   GetNum := Value;
   SkipWhite;
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;


{---------------------------------------------------------------}
{ Parse and Translate a Identifier }

procedure Ident;
var Name: string[8];
begin
   Name:= GetName;
   if Look = '(' then begin
      Match('(');
      Match(')');
      EmitLn('BSR ' + Name);
      end
   else
      EmitLn('MOVE ' + Name + '(PC),D0');
end;


{---------------------------------------------------------------}
{ Parse and Translate a Math Factor }

procedure Expression; Forward;

procedure Factor;
begin
   if Look = '(' then begin
      Match('(');
      Expression;
      Match(')');
      end
   else if IsAlpha(Look) then
      Ident
   else
      EmitLn('MOVE #' + GetNum + ',D0');
end;


{--------------------------------------------------------------}
{ Recognize and Translate a Multiply }

procedure Multiply;
begin
   Match('*');
   Factor;
   EmitLn('MULS (SP)+,D0');
end;


{-------------------------------------------------------------}
{ Recognize and Translate a Divide }

procedure Divide;
begin
   Match('/');
   Factor;
   EmitLn('MOVE (SP)+,D1');
   EmitLn('EXS.L D0');
   EmitLn('DIVS D1,D0');
end;


{---------------------------------------------------------------}
{ Parse and Translate a Math Term }

procedure Term;
begin
   Factor;
   while Look in ['*', '/'] do begin
      EmitLn('MOVE D0,-(SP)');
      case Look of
       '*': Multiply;
       '/': Divide;
      end;
   end;
end;


{--------------------------------------------------------------}
{ Recognize and Translate an Add }

procedure Add;
begin
   Match('+');
   Term;
   EmitLn('ADD (SP)+,D0');
end;


{-------------------------------------------------------------}
{ Recognize and Translate a Subtract }

procedure Subtract;
begin
   Match('-');
   Term;
   EmitLn('SUB (SP)+,D0');
   EmitLn('NEG D0');
end;


{---------------------------------------------------------------}
{ Parse and Translate an Expression }

procedure Expression;
begin
   if IsAddop(Look) then
      EmitLn('CLR D0')
   else
      Term;
   while IsAddop(Look) do begin
      EmitLn('MOVE D0,-(SP)');
      case Look of
       '+': Add;
       '-': Subtract;
      end;
   end;
end;


{--------------------------------------------------------------}
{ Parse and Translate an Assignment Statement }

procedure Assignment;
var Name: string[8];
begin
   Name := GetName;
   Match('=');
   Expression;
   EmitLn('LEA ' + Name + '(PC),A0');
   EmitLn('MOVE D0,(A0)')
end;


{--------------------------------------------------------------}
{ Initialize }
                            
procedure Init;
begin
   GetChar;
   SkipWhite;
end;


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

begin
   Init;
   Assignment;
   If Look <> CR then Expected('NewLine');
end.
{--------------------------------------------------------------}

现在语法分析程序已经完成.它已具有我们可以放入一个直线型"编译器"的所有特征.把它收藏在一个安全的地方.下一次,我们将开始一个新的主题,但一会我们也仍将讨论表达式.下一部分,我打算讲述与编译程序不同的解释程序,并向你展示当我们改动形为的种类时语法分析器的结构变动.即使你对解释程序不感兴趣,但获取这些信息为我们以后服务是很有好处的.下次再见.

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值