编译原理(二)

一、简单表达式的翻译器

通过上一篇知道,预测语法分析不能处理左递归的文法(比如对stmt分析,则遇到多个以stmt开头的产生式,这使得向前看符号无法匹配一个分支,而右递归则是可以使用预测分析法处理)。

1. 抽象语法和具体语法

抽象语法:每个内部结点代表一个运算符,该结点的子结点表示这个运算符的分量。比如表达式 9 -5 + 2,其抽象语法树为

将前面介绍的使用终结符和非终结符表示的语法分析树称为具体语法树,相应的文法为具体语法。这里介绍的为抽象语法树。

考虑前文介绍的一个文法:

expr -> expr1 + term {print(‘+‘)}
expr -> expr1 - term {print(‘-‘)}
expr -> term
term -> 0                 {print(‘0‘)}
term -> 1                 {print(‘1‘)}
...
term -> 9                 {print(‘9‘)}

这个文法包含了左递归,消除方法很简单,转为右递归即可,即产生式 A -> Aα | Aβ | γ 转换为

A -> γR

R -> αR | βR | ε

如果包含内嵌动作,则翻译方案为

expr -> term rest
rest -> + term { print(‘+‘) } rest
    | - term { print(‘-‘) } rest
    | ε
term -> 0 { print(‘0‘) }
    | 1 { print(‘1‘) }
    ...
    | 9 {print(‘9‘) }

注意保持语义动作的顺序,这里动作{print(‘+‘)}和{print(‘-‘)}都处于term和rest中间。

如果这个动作放在rest后面,翻译就不正确了,比如对于表达式 9 - 5 + 2,9为一个term,此时执行动作{print(‘9‘)},然后 “-” 匹配rest的第二个产生式,然后是5,这个term执行动作{print(‘5‘)},然后又是rest非终结符,此时5后面遇到“+”,匹配rest的第一个产生式,然后遇到字符 2,执行{print(‘2‘)},然后需要匹配rest,由于2后面没有字符了,故匹配rest的第三个产生式,没有任何动作,此时返回到rest的第一个产生式,然后执行rest后面的动作{print(‘+‘)},然后再返回到rest的第二个产生式,执行动作{print(‘-‘)},最终翻译结果为 952+-,显然与正确答案95-2+不一致。

用伪代码表示翻译过程,

void expr() {
    term(); rest();
}

void rest() {
    if (lookahead == ‘+‘) {
        match(‘+‘); term(); print(‘+‘); rest();
    }
    else if (lookahead == ‘-‘) {
        match(‘-‘); term(); print(‘-‘); rest();
    }
    else {}
}

void term() {
    if (IsNumber(lookahead)) {
        t = lookahead; match(lookahead); print(t);
    }
    else report ("语法错误“);
}

2. 翻译器的简化

上面伪代码中的rest()函数中,当向前看符号为‘+‘或‘-‘时,对rest()的调用是尾递归调用,此时rest的代码可以改写为

void rest() {
    while(true) {
        if(lookahead == ‘+‘) {
            match(‘+‘); term(); print(‘+‘); continue;
        }
        else if(lookahead == ‘-‘) {
            match(‘-‘); term(); print(‘-‘); continue;
        }
        break;
    }
}

完整的程序为

/* A simple translator written in C#
 */
using System;
using System.IO;

namespace CompileDemo
{
    class Parser
    {
        static int lookahead;
        public Parser()
        {
            lookahead = Console.Read();
        }

        public void expr()
        {
            term();
            while(true)
            {
                if (lookahead == ‘+‘)
                {
                    match(‘+‘);
                    term();
                    Console.Write(‘+‘);
                }
                else if (lookahead == ‘-‘)
                {
                    match(‘-‘);
                    term();
                    Console.Write(‘-‘);
                }
                else
                    return;
            }
        }

        void term()
        {
            if (char.IsDigit((char)lookahead))
            {
                Console.Write((char)lookahead);
                match(lookahead);
            }
            else
                throw new Exception("syntax error");
        }
        void match(int t)
        {
            if (lookahead == t)
                lookahead = Console.Read();
            else
                throw new Exception("syntax error");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("please input an expression composed with numbers and operating chars...\n");
            var parser = new Parser();
            parser.expr();
            Console.Write(‘\n‘);
        }
    }
}    

测试结果为

二、词法分析

假设允许表达式中出现数字、标识符和“空白”符,以扩展上一节介绍的表达式翻译器。扩展后的翻译方案如下

expr -> expr + term {print(‘+‘) }
        |   expr - term { print(‘-‘) }
        |   term

term -> term * factor { print(‘*‘) }
        |    term / factor  { print(‘/‘) }
        |    factor

factor -> (expr)
        | num        { print (num.value) }
        | id            { print(id.lexeme) }

我们对终结符附上属性,如num.value,表示num的值(整数值),而id.lexeme表示终结符id的词素(字符串)。

1. 常量

在输入流中出现一个数位序列时,词法分析器将向语法分析器传送一个词法单元,这个词法单元包含终结符(比如num)以及根据这些数位计算得到的整形属性值,比如输入 31+28+59 被转换为

<num,31><+><num,28><+><num,59>

运算符+没有属性。实现数位序列转换为数值属性的伪代码为

if (peek holds a digit) {
    v = 0;
    do {
            v = v*10 + integer value of digit peek;
            peek = next input character;
    }while(peek holds a digit);
    return token<num, v>;

2. 关键字和标识符

假设将标识符用id终结符表示,则输入如下时,count = count + increment; 语法分析器处理的是终结符序列 id = id + id; 用一个属性保存id的词素,则这个终结符序列写成元组形式为

<id,"count"><=><id,"count"><+><id,"increment"><;>

关键字可作为一种特殊的标识符,为了跟用户输入的标识符区分,通常是将关键字作为保留字,只有不与任何保留字相同的标识符才是用户标识符。区分关键字和标识符的伪代码如下

假设使用类Hashtable将一个字符串表实现为一个散列表。

if(peek holds a alphebet) {
    while (IsAlphaOrDigit(c = IO.Read()))
         buffer.Add(c);    // read a character into buffer
    string s = buffer.ToString();
    w =words.get(s);
    if(w != null) return w;    // reserved word or cached user id
    else {
                words.Add(s, <id, s>);
                return <id, s>;
    }
}

这段代码中,保留字是最开始的时候初始化到words中,以后每次读取到新的用户输入标识符,则进行缓存。

完整的代码如下

 /* A lexer written in C#
*/
    class Lexer
    {
        public int line = 1;
        private char peek = ‘ ‘;
        private Hashtable words = new Hashtable();

        void reserve(Word w)
        {
            words.Add(w.lexeme, w);
        }
        public Lexer()
        {
            reserve(new Word(Tag.TRUE, "true"));
            reserve(new Word(Tag.FALSE, "false"));
        }
        public Token Scan()
        {
            for(;;peek = (char)Console.Read())
            {
                if (peek == ‘ ‘ || peek == ‘\t‘)
                    continue;
                else if (peek == ‘\n‘)
                    line++;
                else
                    break;
            }

            if(char.IsDigit(peek))
            {
                int v = 0;
                do
                {
                    v = 10 * v + peek - ‘1‘ + 1;
                    peek = (char)Console.Read();
                } while (char.IsDigit(peek));
                return new Num(v);
            }

            if(char.IsLetter(peek))
            {
                StringBuilder sb = new StringBuilder();
                do
                {
                    sb.Append(peek);
                    peek = (char)Console.Read();
                } while (char.IsLetterOrDigit(peek));
                string s = sb.ToString();
                Word w = null;
                if (words.Contains(s))
                    w = (Word)words[s];

                if (w != null)  // reserved word or cached ID
                    return w;

                w = new Word(Tag.ID, s);
                words.Add(s, w);
                return w;
            }

            Token t = new Token(peek);
            peek = ‘ ‘;
            return t;
        }
    }

    class Token
    {
        public readonly int tag;
        public Token(int t)
        {
            tag = t;
        }
    }

    class Tag
    {
        public readonly static int NUM = 256, ID = 257, TRUE = 258, FALSE = 259;
    }

    class Num : Token
    {
        public readonly int value;
        public Num(int v) : base(Tag.NUM)
        {
            value = v;
        }
    }
    class Word : Token
    {
        public readonly string lexeme;
        public Word(int t, string s) : base(t)
        {
            lexeme = s;
        }
    }

三、符号表

每个块有自己的符号表。对一个嵌套的块,外层块不能访问内层块的符号表。

比如声明代码片段:

(global var) int w;
...
    int x; int y;  // block 1
        int w; bool y; int z;  // block 2
        ...w, y, z, x...
    w, x, y...

则符号表链为

我们用一个类Env表示语句块的环境,其中有一个成员prev指向这个块的直接外层块的Env环境,并且提供了从当前块的环境中添加符号到符号表的方法,以及从当前符号表向外层块的符号表遍历直到找到字符串对应的符号。

class Env
{
    private Hashtable table;
    protected Env prev;
    public Env(Env p)
    {
        table = new Hashtable();
        prev = p;
    }

    public void Put(string s, Symbol symbol)
    {
        table.Add(s, symbol);
    }
    public Symbol Get(string s)
    {
        // traverse from inner to outer
        for(Env e = this; e!= null; e=e.prev)
        {
            if (e.table.Contains(s))
                return (Symbol)(e.table[s]);
        }
        return null;
    }
}

class Symbol
{
    public string type { get; set; }
}

那这个Env类如何使用呢?

   program ->                                                {top = null;}                  block
   block   ->    ‘{‘                                         {saved = top;
                                                                  top = new Env(top);
                                                                  print("{"); }
                  decls stmts ‘}‘                            {top = saved;
                                                                   print("}"); }

   decls   ->    decls decl
            |      ε
   decl     ->   type id;                                     {s = new Symbol();
                                                                   s.type = type.lexeme;
                                                                   top.Put(id.lexeme, s);}
   stmts   ->  stmts stmt
            |   ε
   stmt    ->   block
            | factor;                                         { print(";");}
  factor    -> id                                             { s = top.Get(id.lexeme);
                                                                    print(id.lexeme);
                                                                    print(":");
                                                                    print(s.type); }

这个翻译方案,在进入和离开块时分别创建和释放符号表。变量top表示一个符号表链的最顶层符号表。

四、中间代码

1. 语法树的构造

前面介绍了抽象语法树,即由运算符作为结点,其子结点为这个运算符的分量。比如 9 - 5。再比如一个while语句

while (expr) stmt

其中while为一个运算符,看作一个结点,其两个子结点为表达式expr和语句stmt。

可以让所有非终结符都有一个属性n,作为语法树的一个结点,将结点实现为Node类对象。对Node类可以有两个直接子类,Expr和Stmt,分别表示表达式和语句。于是,while构造的伪代码如下

new while(x, y)

考虑如下的伪代码,用于表达式和语句的抽象语法树构造

   program  -> block             {return block.n}

    block   -> ‘{‘ stmts ‘}‘   {return block.n = stmts.n;}

    stmts  -> stmts1 stmt  {stmts.n = new Seq(stmts1.n, stmt.n);}
               |   ε                   { stmts.n = null; }

    stmt   -> expr;             {stmt.n = new Eval(expr.n);}
               |  if(expr) stmt1 {stmt.n = new If(expr.n, stmt1.n);}
               | while(expr)stmt1 {stmt.n = new While(expr.n, stmt1.n;}
               | do stmt1 while(expr);
                                     {stmt.n = new Do(stmt1.n, expr.n);}
               | block              {stmt.n = block.n}

    expr   -> rel = expr1    {expr.n = new Assign(‘=‘, rel.n, expr1.n);}     // right associative
               |  rel                 { expr.n = rel.n;}

    rel      -> rel1 < add     { expr.n = new Rel(‘<‘, rel1.n, add.n);}
               |  rel1 <= add   { expr.n = new Rel(‘≤‘, rel1.n, add.n);}
               |  add                {rel.n = add.n;}

    add    ->  add1 + term  {add.n = new Op(‘+‘, add1.n, term.n);}
               |   term             { add.n = term.n;}

    term   ->  term1*factor {term.n = new Op(‘*‘, term1.n, factor.n);}
               |    factor            {term.n = factor.n;}

    factor   -> (expr)           { factor.n = expr.n;}
                |   num             { factor.n = new Num(num.value);}

2. 静态检查

赋值表达式的左部和右部含义不一样,如 i = i + 1; 表达式左部是用来存放该值的存储位置,右部描述了一个整数值。静态检查要求赋值表达式左部是一个左值(存储位置)。

类型检查,比如

if(expr) stmt

期望表达式expr是boolean型。

又比如对关系运算符rel(例如<)来说,其两个运算分量必须具有相同类型,且rel运算结果为boolean型。用属性type表示表达式的类型,rel的运算分量为E1和E2,rel关系表达式为E,那么类型检查的代码如下

if(E1.type == E2.type) E.type = boolean;
else error;

3. 语句翻译

对语句 if expr then stmt1 的翻译,

对 exrp求值并保存到x中if False x goto afterstmt1的代码after: ...

下面给出类If的伪代码,类If继承Stmt类,Stmt类中有一个gen函数用于生成三地址代码。

class If : Stmt
{
    Expr E; Stmt S;
    public If(Expr x, Stmt y)
    {
        E = x; S = y; after = newlabel();
    }

    public void gen()
    {
        Expr n = E.rvalue();  // calculate right value of expression E
        emit("ifFalse " + n.ToString() + " goto " + after);
        S.gen();
        emit(after + ":");
    }
}

If类中,E, S分别表示if语句的表达式expr以及语句stmt。在源程序的整个抽象语法树构建完毕时,函数gen在此抽象语法树的根结点处被调用。

我们采用一种简单的方法翻译表达式,为一个表达式的语法树种每个运算符结点都生成一个三地址指令,不需要为标识符和常量生成代码,而将它们作为地址出现在指令中。

一个结点x的类为Expr,其运算符为op,并将运算结果值存放在由编译器生成的临时名字(如t)中。故 i - j + k被翻译成

t1 = i - j
t2 = t1 + k

对包含数组访问的情况如 2* a[i],翻译为

t1 = a [i]
t2 = 2 * t1

前面介绍了函数rvalue,用于获取一个表达式的右值,此外用于获取左值的函数为lvalue,它们的伪代码如下

Expr lvalue(x: Expr)
{
    if(x is a Id node) return x;
    else if(x is a Access(y,z) node, and y is a Id node)
    {
        return new Access(y, rvalue(z));
    }
    else error;
}

从代码中看出,如果x是一个标识符结点,则直接返回x,如果x是一个数组访问,比如a[i],x结点则形如Access(y,z),其中Access是Expr的子类,y为被访问的数组名,z表示索引。左值函数比较简单不再赘述。

下面来看右值函数伪代码:

Expr rvalue(x: Expr)
{
    if(x is a Id or Constant node)
        return x;
    else if (x is a Op(op, y, z) or Rel(op, y, z) node)
    {
        t = temperary name;
        generate instruction strings of t = rvalue(y) op rvalue(z);
        return a new node of t;
    }
    else if(x is a Access(y,z) node)
    {
        t = temperary name;
        call lvalue(x), and return a Access(y, z‘) node;
        generate instruction strings of Access(y, z‘);
        return a new node of t;
    }
    else if (x is a Assign(y, z) node)
    {
        z‘ = rvalue(z);
        generate instruction strings of lvalue(y) = z‘
        return z‘;
    }
}

分析,x的右值由以下一种情况的返回值确定:

1)如果表达式x是一个标识符或者一个常量,则直接返回x,如 5,则返回5, a, 则返回a;

2)如果表达式x是一个Op运算符(+、-、*、/等)或者Rel运算符(<, >, ==, <=, >=, !=等),则创建一个临时名字t,并对两个运算分量y,z分别求右值,然后用op运算符计算,将运算结果赋给t,返回t;

3)如果表达式x是一个数组访问,则创建一个临时名字t,对数组访问表达式x求其左值返回一个Access(y,z‘)结点,求x左值,是因为x是Access(y, z),而z是一个表达式,需要对z求右值,然后临时名字t被赋予整个数组访问的右值,返回t;

4)如果表达式x是一个赋值运算,则对z求右值得到z‘,对y求左值,并被赋予z‘值,最后返回z’。

例子:

a[i] = 2*a[j-k]

对这个表达式使用rvalue函数,将生成

t3 = j - k
t2 = a[t3]
t1 = 2*t2
a[i] = t1

注:

本系列文章完全参考编译原理龙书教材。

时间: 2024-10-30 22:20:24

编译原理(二)的相关文章

[Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串

本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成AST 一起来学Vue模板编译原理(二)-AST生成Render字符串 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法 这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascri

[Vue源码]一起来学Vue模板编译原理(一)-Template生成AST

本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成AST 一起来学Vue模板编译原理(二)-AST生成Render字符串 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法 这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascrip

编译原理实验二:LL(1)语法分析器

一.实验要求 不得不想吐槽一下编译原理的实验代码量实在是太大了,是编译原理撑起了我大学四年的代码量... 这次实验比上次要复杂得多,涵盖的功能也更多了,我觉得这次实验主要的难点有两个(其实都是难点...): 1. 提取左公因子或消除左递归(实现了消除左递归) 2. 递归求First集和Follow集 其它的只要按照课本上的步骤顺序写下来就好(但是代码量超多...),下面我贴出实验的一些关键代码和算法思想. 二.基于预测分析表法的语法分析 2.1 代码结构 2.1.1  Grammar类    功

编译原理的实验报告一

实验一 词法分析程序实验 专业 商软2班   姓名 黄仲浩  学号 201506110166 一. 实验目的      编制一个词法分析程序. 二. 实验内容和要求 输入:源程序字符串 输出:二元组(种别,单词符号本身). 三. 实验方法.步骤及结果测试 源程序名:bianyiyuanli.c 可执行程序名:bianyiyuanli.exe 原理分析及流程图 通过一些for循环和while循环进行一个个的翻译. 源程序如下: #include<stdio.h> #include<stri

学习编译原理

刚刚进入大二,初学习到编译原理,一门新的技术,而且学习起来会比较抽象,不过好在大一曾学习到VC这一门东西,在学习此门课程之前,已在网上了解到不少学习这门课该提前遇到道德东西,也了解到很多人学习这门课的问题,在他们的经验中知道了不少学习方法,希望自己能在日后中用得上.例如这个就觉得很不错:删繁就简,避重就轻.网上流传较广的一篇<编译原理学习导论>(作者四川大学唐良)就基本是这种思路,对于词法分析,作者避免了自动机理论和集合论推演的介绍,直接搬出源码来,大大降低了理解难度,对于语法分析,作者介绍了

对于学习编译原理

当知道要学习编译原理这门课程的时候,我并没有太多的感觉,觉得看着它就像看当初看导论一样,纯理论的知识就想草率的混过期末就可以.可是看到老师给我们看其他网站上对编译原理的解释时候,就觉得这门课真的很难很难,而且重要.简单浏览下专业人士和非专业人士对于编译的理解后,现在对于编译原理有了一个初步的认知,那就是学了编译原理,你更能够看懂代码,会有更深入本质性的认识,知道如何写代码会比较好一些.学习了编译原理不一定要你写出一个编译器,当然最好是自己写一个,更重要的是你要了解里面的编译思想.看了许多人说哪个

了解编译原理-笔记小结

这是之前学习编译原理过程中做下的笔记. 因能力有限,在很多地方都理解不到位,特别是对于词法分析与语法分析的过程感觉特别晦涩. 分享这个笔记也是为了自己做个总结,算是一个小的提纲吧,都没怎么深入解析编译的过程. 等以后领悟更多了再作补充吧. 希望各路人士能多加指点,谢谢. 词法分析 作用:将输入转换为一个一个的token,而其用一串整数来表示. 协作:只有当解析器需要的时候才会请求词法分析器,继续扫描输入流,在这个过程中将不断生成符号表. 实现:在通常的编程语言中,相对于不确定的有限自动机(NFA

用antlr4来实现《按编译原理的思路设计的一个计算器》中的计算器

上次在公司内部讲<词法分析--使用正则文法>是一次失败的尝试--上午有十几个人在场,下午就只来了四个听众. 本来我还在构思如何来讲"语法分析"的知识呢,但现在看来已不太可能. 这个课程没有预想中的受欢迎,其原因可能是: 1.课程内容相对复杂,听众知识背景与基础差异比较大. 2.授课技巧不够,不能把复杂的知识简单化的呈现给基础稍差一点的人. 针对这两个可能的原因,我要尝试做出以下调整: 1.使用antlr来实现词法和语法的部分. 2.暂时把"编译"过程改为

wex5 教程 前端UI编译原理与记事本编辑

一 前言 wex5页面,与html页面有何差异?两者之前的关系是什么?是如何完成转译的? 能否像编辑html那样用记事本来修改w页面? wex5前端UI在云部署后能否在云端进行二次编辑,而不需要在wex5编辑器里修改后再次上传?? 带着这些问题,重新认识wex5的UI设计与编译原理,有助于我们分离前端开发. 二 页面结构分析: wex5页面由w.js,css三个页面构成,具体功能与对应关系如下: 三 编译后页面结构 1 在公有云部署时,要将wex5页面进行编译,得到部署需要的Native下的ww