递归下降语法分析器实现过程

相比词法分析器,构造语法分析器的方法有很多,其中手写起来最简单的自然是递归下降的方法了。

文法

要实现一种语言的分析器,我们需要先写出这种语言的上下文无关文法(Context-free grammer),我选择语言的文法如下:

P -> S $

S -> S ; S
S -> id = EXP
S -> print ( EXPS )

EXPS -> EXP
EXPS -> EXPS , EXP

EXP -> int
EXP -> id
EXP -> EXP OP EXP
EXP -> ( EXP )

OP -> +
OP -> -
OP -> *
OP -> /

其中大写单词表示非终结符,小写字母和符号表示终结符。

处理文法

递归下降分析器的构造方法非常简单,简单来说,就是为每一个非终结符写一个递归函数,函数中对该终结符可能转换成的所有情况的第一个token进行判断,做出对应的处理,例如,S的函数形式如下(要注意的是这个函数是错的,只是为了表现形式,下面会说明):

void S(){
  switch(current_token){
    case S: 
      S(); eat(';'); S(); break;
    case id: 
      eat(id); eat('='); EXP(); break;
    case print:
      eat(print); eat('('); EXPS(); eat(')'); break;
    default:
      error_exit();
  }
}

看起来很简单吧?但是上面的函数是有问题的,函数只会执行case S的情况,并将无限的递归下去,这称为左递归。
这不是我们方法的问题,而是文法格式的问题,递归下降方法只能处理没有左递归和左因子的文法,我们需要消除它。

消除左递归

类似

E -> E + T
E -> T

的文法是左递归的,我们可以通过引入一个新的非终结符来消除它,结果如下:

E -> T E'
E' -> + T
E' ->

上面展示的并不是一种模板,而是一种思想,我们的文法中,在非终结符EXP,S中也出现了左递归的情况,但是要复杂得多,你需要理解消除左递归的思想来解决它们。

提取左因子

考虑文法中的EXPS,我们发现我们无法根据其转换的第一个符号来判断下一步该做什么。
当一个非终结符的不同转换的前几个符号是相同的符号,这种情况也会发生类似左递归的问题,我们需要通过提取左因子的方法消除它。
类似

S -> E + E
S -> E

的文法是有问题的,依然是引入一个新的非终结符:

S -> E S'
S' -> + E
S' ->

最终的文法

在消除了EXP 和 S的左递归,提取了EXPS的左因子之后,文法如下:

P -> S $

S -> id = EXP S'
S -> print ( EXP S ) S'
S' -> ; S
S' -> 

EXPS -> EXP EXPS'
EXPS' -> , EXPS
EXPS' ->

EXP -> ( EXP )
EXP -> int EXP' 
EXP -> id EXP'
EXP' -> OP EXP
EXP' ->

OP -> +
OP -> -
OP -> *
OP -> /

构建分析器

现在让我们从语言到结果,构造我们的代码。

词法分析(myrdp.l)

首先,我们需要把字符串形式的语言转换成token串,这一步我们需要词法分析器,我使用flex完成这一步的工作,在分析过程中存储每个token的类型和值,存储类型的具体定义见下面的 myrdp.h 文件代码,代码如下:

%{
#include "myrdp.h"
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace myrdp;
%}

%%
;       tokens.push_back(Token(SEMICOLON));
=       tokens.push_back(Token(ASSIGN));
\(      tokens.push_back(Token(LPAREN));
\)      tokens.push_back(Token(RPAREN));
,       tokens.push_back(Token(COMMA));
\+      tokens.push_back(Token(PLUS));
-       tokens.push_back(Token(MINUS));
\*      tokens.push_back(Token(TIMES));
\/      tokens.push_back(Token(DIV));
print   tokens.push_back(Token(PRINT));
[0-9]+  tokens.push_back(Token(INT,atoi(yytext)));
[a-zA-Z]+[a-zA-Z0-9]* {
  tokens.push_back(Token(ID,std::string(yytext)));
}
[ \t\n] ;
.       std::cout<<"invalid input:"<<yytext<<std::endl;
%%
int yywrap(){
  return 1;
}

语法分析(myrdp.h myrdp.cpp)

现在我们得到了一个存储了token的vector,我们只要按照上面所说的构建方法,从头到尾以递归的方式遍历一遍vector就可以得到结果,代码很简单,直接看代码吧。
程序的入口结构,即写在main函数里的部分

yyin = fopen(argv[1],"r");
yylex();
prog();

关键结构的定义和函数声明见myrdp.h

myrdp.h

#pragma once

#include <vector>
#include <string>
#include <map>

namespace myrdp{
/*token 类型*/
enum Type{
  SEMICOLON=1,  ASSIGN,   PRINT,
  LPAREN,     RPAREN,   COMMA,
  INT,        ID,       PLUS,
  MINUS,      TIMES,    DIV
};
/*token值存储*/
struct Value{
  int num;
  std::string id;
  Value() = default;
  Value(int _n):num(_n){}
  Value(const std::string& _i):id(_i){}
  ~Value(){}
};
/*token结构*/
struct Token{
  Type type;
  Value value;
  Token(Type _t):type(_t),value(0){}
  Token(Type _t,int _v):type(_t),value(_v){}
  Token(Type _t,const std::string& _i):type(_t),value(_i){}
};

extern std::vector<Token> tokens;
extern std::map<std::string,int> table;
extern int cur;
extern std::vector<std::string> TypeToString;

void prog();
void stm();
void stm_prime();
void exps();
void exps_prime(int);
int exp();
int exp_prime(int);
}

具体函数和变量的定义

myrdp.cpp


#include <vector>
#include <cstdlib>
#include <iostream>
#include <map>
#include "myrdp.h"

namespace myrdp{

std::vector<Token> tokens;
std::map<std::string,int> table;
int cur = 0;

std::vector<std::string> TypeToString = {
  "0","SEMICOLON",  "ASSIGN",   "PRINT",
  "LPAREN",     "RPAREN",   "COMMA",
  "INT",        "ID",       "PLUS",
  "MINUS",      "TIMES",    "DIV"
};
//prog,stm,exps,exp
void err_exit(){
  std::cout<<"syntax error in token: ";
  std::cout<<TypeToString[myrdp::tokens[myrdp::cur].type]<<std::endl;
  exit(EXIT_FAILURE);
}
void eat(Type t){
  if(t==myrdp::tokens[myrdp::cur].type){
    myrdp::cur++;
  }else{
    err_exit();
  }
}

void prog(){
  stm();
}

void stm(){
  const Token &tok = myrdp::tokens[myrdp::cur];
  switch(tok.type){
    case ID:
      myrdp::cur++;
      eat(ASSIGN);
      myrdp::table[tok.value.id]=exp();
      stm_prime();
      break;
    case PRINT:
      eat(PRINT);
      eat(LPAREN);
      exps();
      eat(RPAREN);
      stm_prime();
      break;
    default:
      err_exit();
  }
}

void stm_prime(){
  const Token &tok = myrdp::tokens[myrdp::cur];
  switch(tok.type){
    case SEMICOLON:
      eat(SEMICOLON);
      stm();
      break;
    default: 
      break;
  }
}

void exps(){
  exps_prime(exp());
}

void exps_prime(int a){
  const Token &tok = myrdp::tokens[myrdp::cur];
  switch(tok.type){
    case COMMA:
      eat(COMMA);
      exps_prime(exp());
      break;
    default:
      std::cout<<a<<" ";
      break;
  }
}

int exp(){
  const Token &tok = myrdp::tokens[myrdp::cur];
  int a;
  switch(tok.type){
    case LPAREN:
      eat(LPAREN);
      a = exp();
      eat(RPAREN);
      return a;
    case ID:
      a = myrdp::table[tok.value.id];myrdp::cur++;
      return exp_prime(a); 
    case INT:
      a = tok.value.num;cur++;
      return exp_prime(a);
    default:
      err_exit();
      break;
  }
  //cannot reach here
  return 0;
}

int exp_prime(int a){
  const Token &tok = myrdp::tokens[myrdp::cur];
  switch(tok.type){
    case PLUS:
      eat(PLUS);
      return a+exp();
    case MINUS:
      eat(MINUS);
      return a-exp();
    case TIMES:
      eat(TIMES);
      return a*exp();
    case DIV:
      eat(DIV);
      return a/exp();
    default: 
      return a;
  }
}
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容