AQL Subset Compiler:手把手教你如何写一个完整的编译器
项目地址: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:手把手教你如何写一个完整的编译器的更多相关文章
- 手把手教你手写一个最简单的 Spring Boot Starter
欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ...
- java nio 写一个完整的http服务器 支持文件上传 chunk传输 gzip 压缩 使用过程 和servlet差不多
java nio 写一个完整的http服务器 支持文件上传 chunk传输 gzip 压缩 也仿照着 netty处理了NIO的空轮询BUG 本项目并不复杂 代码不多 ...
- [原创作品]手把手教你怎么写jQuery插件
这次随笔,向大家介绍如何编写jQuery插件.啰嗦一下,很希望各位IT界的‘攻城狮’们能和大家一起分享,一起成长.点击左边我头像下边的“加入qq群”,一起分享,一起交流,当然,可以一起吹水.哈,不废话 ...
- 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)
本文由作者FreddyChen原创分享,为了更好的体现文章价值,引用时有少许改动,感谢原作者. 1.写在前面 一直想写一篇关于im即时通讯分享的文章,无奈工作太忙,很难抽出时间.今天终于从公司离职了, ...
- 教你如何写一个 Yii2 扩展
前言 把一系列相关联的功能使用模块开发,好处多多,维护起来很方便,模块还可以单独发布出去,让下一个项目之间使用,真是方便. 下面我就写一个开发扩展的简单教程. Gii gii 自带帮助我们生成一个基本 ...
- 宜信开源|手把手教你安装第一个LAIN应用
LAIN是宜信公司大数据创新中心开发的开源PaaS平台.在金融的场景下,LAIN 是为解放各个团队和业务线的生产力而设计的一个云平台.LAIN 为宜信大数据创新中心各个团队提供了统一的测试和生产环境, ...
- Istio技术与实践04:最佳实践之教你写一个完整的Mixer Adapter
Istio内置的部分适配器以及相应功能举例如下: circonus:微服务监控分析平台. cloudwatch:针对AWS云资源监控的工具. fluentd:开源的日志采集工具. prometheus ...
- Mark: 如何用Haskell写一个简单的编译器
作者:aaaron7 链接:https://www.zhihu.com/question/36756224/answer/88530013 如果是用 Haskell 的话,三篇文章足矣. prereq ...
- python+selenium+unnitest写一个完整的登陆的验证
import unittest from selenium import webdriver from time import sleep class lonInTest (unittest.Test ...
随机推荐
- 初学时的shell
学习期间写过一些shell脚本, 测试过程:vi test.sh 后把程序写入其中,保存退出.然后改变文件属性:chmod +x test.sh 最后执行:./test.shfor语句测试:1)#!/ ...
- Ubuntu常用终端快捷键
CTRL+k:删除从光标到行尾的部分 CTRL+u:删除从光标到行首的部分 ALT+d:删除从光标到当前单词结尾的部分 CTRL+w:删除从光标到当前单词开头的部分 CTRL+a:将光标移到行首 CT ...
- jQuery无刷新上传学习心得
记得刚离开大学,进入目前这家公司不到一个月时,有一位前辈给我们当时的新人讲了下JS无刷新上传的相关知识. 在此之前,一直都是在使用C#提供的服务器上传控件FileUpload,但是每次使用时,都会刷新 ...
- WPF 窗体中的 Canvas 限定范围拖动 鼠标滚轴改变大小
xaml代码: <Canvas Name="movBg"> <Canvas.Background> <LinearGradientBrush EndP ...
- a+b(高精度)[陈涛]——c语言
#include<stdio.h>#include<string.h>char s[1000];---------------------------------------- ...
- Win32中GDI+应用(五)--GDI与GDI+编程模型的区别
在GDI里面,你要想开始自己的绘图工作,必须先获取一个device context handle,然后把这个handle作为绘图复方法的一个参数,才能完成任务.同时,device context ha ...
- 网站开发常用jQuery插件总结(四)验证插件validation
在网站开发过程中,有时我们需要验证用户输入的信息是否符合我们的要求,所以我们会对用户提交的数据进行验证.验证分两次进行,一次是在客户端,一次是在服务端.客户端的验证可以提升用户的体验. jquery验 ...
- void (*f(int, void (*)(int)))(int) 函数解析 转
今天与几个同学看到了一个函数指针定义: void (*f(int, void (*)(int)))(int) 以前在C trap pit fails里面见过,但是文章里面介绍的很详细,但是往往使初学者 ...
- ASP.NET MVC轻教程 Step By Step 6——改进表单
上一节我们使用原始的HTML表单来完成留言功能,但是ASP.NET MVC提供了丰富的HTML辅助方法来帮助我们构建更简洁优雅的表单. Step 1. 修改Form标签 首先,我们可以使用Html.B ...
- 总结Web应用中基于浏览器的安全漏洞
1.浏览器缓存 每次打开一个网站,网页的内容会缓存到用户的机器中.如果这些内容在其他网页中需要重新加载,浏览器加载的是缓存,而不是再次下载内容.如果一些Web应用商店以及显示用户敏感信息(比 ...