项目地址:https://github.com/laiy/Awesome-Complier

转载请注明出处。

前言

这是学校里编译原理课程的大作业,此Project十分适合编译原理的学习,让基本不听课的我理解了一个编译器的编写过程。

所以忍不住想分享一下。

什么是AQL?

全称: Annotation Query Language

用于Text Analytics。

可以从非结构化或半结构化的文本中提取结构化信息的语言。

语法与SQL类似。

什么是AQL Subset?

AQL语法复杂,功能强大,实现难度较高,作为学习用,我们选择实现AQL的部分语法功能达到学习编译器编写的效果。

AQL Subset具有AQL的主要特点。

主要实现以下功能:

1. 通过regex来生成一个view。

2. 通过pattern来拼接多个view或者正则表达式处理的结果。

3. 通过select来选择已有view的列生成新的view。

4. 打印view的结果。

例子:PerLoc.aql

 create view Cap as
extract regex /[A-Z][a-z]*/
on D.text as Cap
from Document D; create view Stt as
extract regex /Washington|Georgia|Virginia/
on D.text
return group 0 as Stt
from Document D; create view Loc as
extract pattern (<C.Cap>) /,/ (<S.Stt>)
return group 0 as Loc
and group 1 as Cap
and group 2 as Stt
from Cap C, Stt S; create view Per as
extract regex /[A-Z][a-z]*/
on D.text
return group 0 as Per
from Document D; create view PerLoc as
extract pattern (<P.Per>) <Token>{1,2} (<L.Loc>)
return group 0 as PerLoc
and group 1 as Per
and group 2 as Loc
from Per P, Loc L; create view PerLocOnly as
select PL.PerLoc as PerLoc
from PerLoc PL; output view Cap;
output view Stt;
output view Loc;
output view Per;
output view PerLoc;
output view PerLocOnly;

假设处理的文本内容是:

Carter from Plains, Georgia, Washington from Westmoreland, Virginia

那么输出结果为:

Processing ../dataset/perloc/PerLoc.input
View: Cap
+----------------------+
| Cap |
+----------------------+
| Carter:(0,6) |
| Plains:(12,18) |
| Georgia:(20,27) |
| Washington:(29,39) |
| Westmoreland:(45,57) |
| Virginia:(59,67) |
+----------------------+
6 rows in set View: Stt
+--------------------+
| Stt |
+--------------------+
| Georgia:(20,27) |
| Washington:(29,39) |
| Virginia:(59,67) |
+--------------------+
3 rows in set View: Loc
+--------------------------------+----------------------+--------------------+
| Loc | Cap | Stt |
+--------------------------------+----------------------+--------------------+
| Plains, Georgia:(12,27) | Plains:(12,18) | Georgia:(20,27) |
| Georgia, Washington:(20,39) | Georgia:(20,27) | Washington:(29,39) |
| Westmoreland, Virginia:(45,67) | Westmoreland:(45,57) | Virginia:(59,67) |
+--------------------------------+----------------------+--------------------+
3 rows in set View: Per
+----------------------+
| Per |
+----------------------+
| Carter:(0,6) |
| Plains:(12,18) |
| Georgia:(20,27) |
| Washington:(29,39) |
| Westmoreland:(45,57) |
| Virginia:(59,67) |
+----------------------+
6 rows in set View: PerLoc
+------------------------------------------------+--------------------+--------------------------------+
| PerLoc | Per | Loc |
+------------------------------------------------+--------------------+--------------------------------+
| Carter from Plains, Georgia:(0,27) | Carter:(0,6) | Plains, Georgia:(12,27) |
| Plains, Georgia, Washington:(12,39) | Plains:(12,18) | Georgia, Washington:(20,39) |
| Washington from Westmoreland, Virginia:(29,67) | Washington:(29,39) | Westmoreland, Virginia:(45,67) |
+------------------------------------------------+--------------------+--------------------------------+
3 rows in set View: PerLocOnly
+------------------------------------------------+
| PerLoc |
+------------------------------------------------+
| Carter from Plains, Georgia:(0,27) |
| Plains, Georgia, Washington:(12,39) |
| Washington from Westmoreland, Virginia:(29,67) |
+------------------------------------------------+
3 rows in set

很容易看懂,Cap这个view提取了大写字母开头的英文单词,Stt提取了美国洲名的单词,Loc对Cap和Stt进行拼接,按照中间只隔了一个逗号,且后一个单词为州名为一个地名的规则得到地名。

其中group 0指的是匹配的结果,group 1, 2, 3...指的是匹配规则中括号的内容。

然后Per人名假设和Cap一样,那么PerLoc则是拼接了Per和Loc,指定中间间隔1到2个Token(以字母或者数字组成的无符号分隔的字符串,或者单纯的特殊符号,不包含空白符。)。

最后PerLocOnly则是从view PerLoc中select了一个列出来。

其中配个匹配后面的(x, y)指的是匹配的字符在原文中的位置,左闭右开。

然后output view xxx就是把提取出的view打印出来得到了以上的结果。

Language

aql_stmt → create_stmt ; | output_stmt ;
create_stmt → create view ID as view_stmt
view_stmt → select_stmt | extract_stmt
output_stmt → output view ID alias
alias → as ID | ε
elect_stmt → select select_list from from_list
select_list → select_item | select_list , select_item
select_item → ID . ID alias
from_list → from_item | from_list , from_item
from_item → ID ID
extract_stmt → extract extract_spec from from_list
extract_spec → regex_spec | pattern_spec
regex_spec → regex REG on column name_spec
column → ID . ID
name_spec → as ID | return group_spec
group_spec → single_group | group_spec and single_group
single_group → group NUM as ID
pattern_spec → pattern pattern_expr name_spec
pattern_expr → pattern_pkg | pattern_expr pattern_pkg
pattern_pkg → atom | atom { NUM , NUM } | pattern_group
atom→ < column > | < Token > | REG
pattern_group → ( pattern_expr )

以上为AQL Subset的文法,这是语法分析生成抽象语法树的规则。

从文法可以看出,所有文法左边的为非终结符,而其他的关键词为终结符(语法数的叶子节点)。

编译器结构

词法分析器(Lexer)

首先要把AQL语言源文件输入到词法分析器中,词法分析器的职责就是从一个字符串中提取出AQL语言的非终结符序列,然后这个非终结符序列作为输入提供给语法分析器解析生成抽象语法树。

那Lexer要做的事情就很清晰了,首先定义一个token(非终结符)的数据结构如下:

 struct token {
std::string value;
Type type;
bool is_grouped;
token(std::string value, Type type) {
this->value = value;
this->type = type;
this->is_grouped = false;
}
bool operator==(const token &t) const {
return this->value == t.value && this->type == t.type;
}
};

value是匹配到的字符串,Type为token的类型,is_grouped是之后语法分析的时候匹配group用的,暂时不管。

然后根据AQL Subset的文法(上面的Language),可以得出非终结符的类型有如下:

typedef enum {
CREATE, VIEW, AS, OUTPUT, SELECT, FROM, EXTRACT, REGEX, ON, RETURN,
GROUP, AND, TOKEN, PATTERN, ID, DOT, REG, NUM, LESSTHAN, GREATERTHAN,
LEFTBRACKET, RIGHTBRACKET, CURLYLEFTBRACKET, CURLYRIGHTBRACKET, SEMICOLON, COMMA, END, EMPTY
} Type;

最后两个END和EMPTY也是方便语法分析用的,并不是一个真实的token类型。

然后定义一个Lexer类:

 class Lexer {
public:
Lexer(char *file_path);
std::vector<token> get_tokens();
private:
std::vector<token> tokens;
};

Lexer在构造函数的时候就把AQL源文件进行处理得到一组token存放在tokens这个vector里,然后提供get_tokens方便语法分析器调用获得到tokens。

具体实现细节我不会在这里讲述,感兴趣的可以回到顶部进入项目代码地址查看源代码。

语法分析器(Parser)

语法分析器做的就是利用词法分析出来的token序列,构建出一个抽象语法树(AST),然后传递给编译器后端执行(Lexer + Parser我们通常称为编译器的前端)。

编译器后端做的主要是根据语法树的结构,完成具体的执行逻辑。

这里需要注意!很多同学混淆不清的一个问题:这里所说的构建抽象语法树并不是在数据结构上去构建一颗树出来,这里的树的意思体现在函数的调用逻辑,举个例子,一个简单的DFS搜索,递归实现,这个搜索经常会呈现出一个树的逻辑结构。

然后根据文法(Language),我们可以定义一个Parser类:

 class Parser {
public:
Parser(Lexer lexer, Tokenizer tokenizer, const char *output_file, const char *processing);
~Parser();
token scan();
void match(std::string);
void error(std::string str);
void output_view(view v, token alias_name);
void program();
void aql_stmt();
void create_stmt();
std::vector<col> view_stmt();
void output_stmt();
token alias();
std::vector<col> select_stmt();
std::vector<token> select_list();
std::vector<token> select_item();
std::vector<token> from_list();
std::vector<token> from_item();
std::vector<col> extract_stmt();
std::vector<token> extract_spec();
std::vector<token> regex_spec();
std::vector<token> column();
std::vector<token> name_spec();
std::vector<token> group_spec();
std::vector<token> single_group();
std::vector<token> pattern_spec();
std::vector<token> pattern_expr();
std::vector<token> pattern_pkg();
std::vector<token> atom();
std::vector<token> pattern_group();
inline col get_col(view v, std::string col_name);
inline view get_view(std::string view_name);
inline void print_line(view &v);
inline void print_col(view &v);
inline void print_span(view &v);
private:
std::vector<token> lexer_tokens;
int lexer_parser_pos;
token look;
std::vector<document_token> document_tokens;
std::vector<view> views;
FILE *output_file;
};

需要注意的是构造函数里的tokenizer并不是词法分析器,只是AQL需要处理的文本的分词器,作用和Lexer类似,因为在pattern匹配的时候需要用到<Token>的表示。

然后从树的根节点program()开始,不断的根据文法规则和look(预读的token,通过look我们可以唯一的定位到在一个非终结符节点下一步的函数调用)调用,然后利用函数的返回值传递必要的执行参数(我这里主要是以token的vector形式实现)。

这样描述可能有点抽象,看个具体的例子。

来看我们之前例子的第一个create语句:

create view Cap as
extract regex /[A-Z][a-z]*/
on D.text as Cap
from Document D;

我们根据向前看规则,可以得到如下抽象语法树:

绿色的线表示函数调用,黑色表示终结符匹配。

来看预读的token是怎么帮助我们找到正确的函数调用的,以非终结符节点aql_stmt为例:

aql_stmt → create_stmt ; | output_stmt ;

aql_stmt可以推导出create_stmt+;或者output_stmt+;。

然后此时如果look.Type是CREATE的话其实就意味着应该调用create_stmt,如果是output_stmt的话此时预读的token类型应该是OUTPUT。

整体思路就是这样,我们分别把每个非终结符按照文法规则利用预读的look即可完成一个抽象语法树的结构(非终结符的节点箭头指向表示的是函数调用)。

编译器后端

我们在抽象语法树构造的过程中,在必要的节点返回程序执行需要的数据,然后后端做的事情就是利用抽象语法树提取出来的关键数据去执行AQL语言需要实现的逻辑。

而AQL Subset的后端逻辑其实只有4个:

1. 利用正则表达式创建一个view。

2. 利用pattern匹配创建一个view。

3. 利用select提取创建一个view。

4. 打印view。

按理说还应该抽象出一个执行类,提供方法,让Parser直接调用去执行的,由于这里后端逻辑挺简单的我就直接写在抽象语法树构造过程函数的内部了。

对应分别是:

1. extract_stmt的条件分支(Parser.cpp 213-229行)实现正则提取文本。

2. extract_stmt的条件分支(Parser.cpp 234-320行)实现pattern匹配提取文本。

3. select_stmt尾部实现select逻辑。

4. output_view函数实现打印逻辑。

5. view的创建逻辑实现在create_stmt尾部。

具体实现没什么好说的了,coding就是了。

完成之后可以自己玩各种有趣的文本处理,比如我从HTML文本中提取出所有meta的内容,又比如可以提取出HTML中所有的class, id等等....(结果可以参考dataset/html/*.output,提取的aql源文件为dataset/html.aql)。

一些体会

一个编译器的编写完全体现了自顶向下,逐步求精的分而治之的架构思想,其实具体coding对于大三的学生来说已经完全不是什么难事了,重要的是怎么把一个大的问题不断分治到能够被轻易解决的小的问题上来。

谢谢。

AQL Subset Compiler:手把手教你如何写一个完整的编译器的更多相关文章

  1. 手把手教你手写一个最简单的 Spring Boot Starter

    欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ...

  2. java nio 写一个完整的http服务器 支持文件上传 chunk传输 gzip 压缩 使用过程 和servlet差不多

    java nio 写一个完整的http服务器  支持文件上传   chunk传输    gzip 压缩      也仿照着 netty处理了NIO的空轮询BUG        本项目并不复杂 代码不多 ...

  3. [原创作品]手把手教你怎么写jQuery插件

    这次随笔,向大家介绍如何编写jQuery插件.啰嗦一下,很希望各位IT界的‘攻城狮’们能和大家一起分享,一起成长.点击左边我头像下边的“加入qq群”,一起分享,一起交流,当然,可以一起吹水.哈,不废话 ...

  4. 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)

    本文由作者FreddyChen原创分享,为了更好的体现文章价值,引用时有少许改动,感谢原作者. 1.写在前面 一直想写一篇关于im即时通讯分享的文章,无奈工作太忙,很难抽出时间.今天终于从公司离职了, ...

  5. 教你如何写一个 Yii2 扩展

    前言 把一系列相关联的功能使用模块开发,好处多多,维护起来很方便,模块还可以单独发布出去,让下一个项目之间使用,真是方便. 下面我就写一个开发扩展的简单教程. Gii gii 自带帮助我们生成一个基本 ...

  6. 宜信开源|手把手教你安装第一个LAIN应用

    LAIN是宜信公司大数据创新中心开发的开源PaaS平台.在金融的场景下,LAIN 是为解放各个团队和业务线的生产力而设计的一个云平台.LAIN 为宜信大数据创新中心各个团队提供了统一的测试和生产环境, ...

  7. Istio技术与实践04:最佳实践之教你写一个完整的Mixer Adapter

    Istio内置的部分适配器以及相应功能举例如下: circonus:微服务监控分析平台. cloudwatch:针对AWS云资源监控的工具. fluentd:开源的日志采集工具. prometheus ...

  8. Mark: 如何用Haskell写一个简单的编译器

    作者:aaaron7 链接:https://www.zhihu.com/question/36756224/answer/88530013 如果是用 Haskell 的话,三篇文章足矣. prereq ...

  9. python+selenium+unnitest写一个完整的登陆的验证

    import unittest from selenium import webdriver from time import sleep class lonInTest (unittest.Test ...

随机推荐

  1. 检测SqlServer服务器IO是否瓶颈

    通过性能监视器监视 Avg. Disk Queue Length   小于2 Avg. Disk sec/Read , Avg. Disk sec/Write  小于10ms 可以用数据收集器定时收集 ...

  2. oracle rowid 使用

    ROWID是数据的详细地址,通过rowid,oracle可以快速的定位某行具体的数据的位置. ROWID可以分为物理rowid和逻辑rowid两种.普通的堆表中的rowid是物理rowid,索引组织表 ...

  3. Fast Report Data Filter

    使用Data Filter两种方式:一种是 直接在Filter 属性里写表达式 ,另外一种就是在beforePrint 事件里写方法. 今天开发时遇到了一个Filter的问题,不知道是不是fast r ...

  4. mysql修改字符集 转载

    查看编码:    show variables like 'collation_%';    show variables like 'character_set_%';    修改:    MySQ ...

  5. KMP算法——字符串匹配

    正直找工作面试巅峰时期,有幸在学校可以听到July的讲座,在时长将近三个小时的演讲中,发现对于找工作来说,算法数据结构可以算是程序员道路的一个考量吧,毕竟中国学计算机的人太多了,只能使用这些方法来淘汰 ...

  6. JavaScript 学习笔记之线程异步模型

    核心的javascript程序语言并没有包含任何的线程机制,客户端javascript程序也没有任何关于线程的定义,事件驱动模式下的javascript语言并不能实现同时执行,即不能同时执行两个及以上 ...

  7. js 中对象--对象结构(原型链基础解析)

    对于本篇对于如何自定义对象.和对象相关的属性操作不了解的话,可以查我对这两篇博客.了解这两篇可以更容易理解本篇文章 用构造函数创建了一个对象  obj对象的本身创建了两个属性  x=1   ,y=2 ...

  8. JavaScript学习心得(二)

    一选择DOCTYPE DOCTYPE是一种标准通用标记语言的文档类型声明,目的是告诉标准通用标记语言解析器使用什么样的文档类型定义(DTD)来解析文档. 网页从DOCTYPE开始,即<!DOCT ...

  9. PHPCMS栏目调用2

    {php $j=1;}                {loop subcat(50) $v}                {php if($v['type']!=0) continue;}     ...

  10. CANoe 入门 Step by step系列(一)基础应用【转】

    CANoe是Vector公司的针对汽车电子行业的总线分析工具,现在我用CANoe7.6版本进行介绍,其他版本功能基本差不多. 硬件我使用的是CAN case XL. 1,CANoe软件的安装很简单,先 ...