21分钟学会写编译器

2023-05-16

本文来自网易云社区。

 

知乎上有一种说法是「编译器、图形学、操作系统是程序员的三大浪漫」。

 

先不管这个说法是对是错,我们假设一个程序员在国内互联网公司写代码,业余时间不看相关书籍。那么三年之后,他的这些知识会比在校时损耗多少?

 

很显然,损耗的比例肯定非常高,毕竟国内互联网公司日常开发工作中,程序员基本很少接触这三块知识。大部分程序员工作几年后对编译原理相关的概念只能生理上起反应,脑海里很难再串联起相关概念了。

 

编译原理的概念有让人看到就头痛的特质,学校里要死记硬背,考试过了巴不得赶紧全忘掉,相信不少同学现在看到下面概念还会觉得蛋疼:

 

  • 非确定性有限自动机/确定性有限自动机
  • 四元式序列
  • 上下文无关文法/BNF
  • 终结符/非终结符
  • LL(1)/LR(1)
  • 特设语法制导转换
  • 局部优化

 

如果要按照课程来,光是背下这些名词和释义别说21分钟了,21天都搞不定。更何况背下来这些名词之后如何写编译器又是另一个问题。

 

我们很多时候,都只是想快速上手写一个编译器,有可能是因为好奇,有可能是想实现自己的玩具DSL(领域特定语言),或者有可能是为了在约架时候防身。

 

今天,我们就来看看如何用21分钟的时间学会写编译器,上面的废话大概花费1分钟,接下来还剩20分钟。

 

正式开始做编译器之前,先以问答的形式对接下来的内容做个简单介绍:

 

  • 什么是编译器

广义的编译器可以指任意把一种语言代码转为另一种语言代码的程序。

 

  • 做编译器实际上都需要做什么

编译器是一整套工具链,从前端的词法分析、语法分析,到中间表示生成、检查、分析、优化,再到代码生成。

 

如果是编译器从业者,大部分时间在做中间这块;如果是业余爱好者,大部分时间在做前端和代码生成。

 

  • 那我们今天做的是个什么编译器

既然是21分钟,那学会写个gcc肯定是不可能的,就算拿来学Flex+Bison都只能入个门。

 

我们接下来会先确定一下源语言的语法集,然后设计一下抽象语法树(AST)结构,写一个Parser,把源语言转换成一棵抽象语法树,再写一个CodeGenerator,把语法树转换为目标语言。

 

也就是说,相比于一个正常的编译器,我们省去了类型检查和中间表示的分析优化,并且为了能21分钟内学会,我们会把源语言定义得特别蠢。

 

接下来开始正文。

 

先确定源语言:

 

这是一门看起来像lisp的四则运算语言,四个双目运算符分别是「add」「sub」「mul」「div」。

 

多项四则运算可以这样写:

 

(mul (sub 5 (add 1 2)) 4)


再来确定目标语言:

 

同样是一门四则运算语言,但是看起来可读性更强,对应的四个双目运算符分别是「+」「-」「*」「/」。

 

上面源语言的例子编译完后应该是这样:

 

((5 - (1 + 2)) * 4)


最后确定我们写编译器要用的语言:

选择Haskell,有两个原因,一是写Haskell有大名鼎鼎的ParseC,写Parser非常方便;二是Haskell的代数数据类型的定义本身就是AST。

 

ParseC的全称是Parser组合子。Parser,抽象理解就是一个输入为字符串输出为类型T的值的函数。ParseC库实现了大量基础Parser和Parser组合子,Parser组合子可以将库自带的基础Parser和用户定义的Parser随意组合成新的更强大的Parser。

 

举个例子,你实现了一个Parser,功能是根据输入文本返回解析到的标识符名称。ParseC库实现了一个名叫many的parser组合子,跟你自己的Parser组合起来就产生了一个新的Parser:可以根据输入文本返回解析到的标识符名称list。

 

为什么要用ParseC呢?因为用ParseC定义Parser具有PEG(解析表达式文法,原理不细讲,不影响接下来学习)的所有好处,同时还不用再学习语言之外的知识(比如用flex和bison前要先学习这两者自己的「DSL」)。

 

当然,其他语言也有类似的库,比如c++有boost::spirit,Java/C#/F#/JS有Haskell的ParseC的工业级实现。这些语言跟Haskell的区别无非在于要写一些额外的逻辑把Parser的解析结果转成AST。

 

如果没有接触过Haskell的话也没关系,接下来的示例代码都非常declarative,非常self-descriptive,请放心食用。

 

接下来就开始写代码了,首先我们要定义AST的结构,目的是为了能用这个结构描述一切源语言表达式。

 

简单分析一下源语言,我们可以直接得出表达式这个概念的递归定义:一个表达式要么是一个字面值,要么是一个双目运算符和两个表达式的求值结果。

 

然后是字面值这个概念的递归定义:一个字面值要么是一个整型值,要么是一个浮点型值。
在Haskell里面这两个定义写成下面这样:

 

type BinOp = String
data Val = IntVal Integer     | FloatVal Float deriving (Eq, Ord, Show)
data Exp =  ConstExp Val     | BinOpExp BinOp Exp Exp deriving (Eq, Ord, Show)


跟前面的文字定义对应一下:

  • 表达式Exp,要么是一个字面值表达式ConstExp,由一个Val组成;要么是一个双目运算表达式BinOpExp,由一个操作符和两个Exp组成。
  • 值Val,要么是一个整型值IntVal,由一个Integer组成;要么是一个浮点型值FloatVal,由一个Float组成。


接下来开始写Parser。流程是先为AST中的每个节点类型写一个parser,然后再把这些parser组合起来形成能parse出整棵AST的parser。
我们先给自己定个小目标,比如先实现一个int_parser。

 

p_int是能从文本中Parse出Integer的Parser定义。而p_int_val改造了p_int,定义了能从文本中Parse出IntVal的Parser。

 

然后我们把int和float的parser组合起来成为一个val_parser。

p_val :: Parser Val
p_val =  listplus [p_int_val,p_float_val]


listplus可以简单理解为并,在具体实现上会做回溯。

 

同理,我们先分别实现ConstExp的parser和BinOpExp的parser,再把两者组合为exp_parser。

p_const_exp :: Parser Exp p_const_exp =  ConstExp <$> p_parse p_val
 <?> "p_const_exp"  p_bin_op_exp :: Parser Exp p_bin_op_exp =  p_between '(' ')' inner <?> "p_bin_op_exp"     where         inner = BinOpExp
 <$> p_parse (listplus [string "add", 
                                string "sub", string "mul", string "div"])
 <*> p_parse p_exp
 <*> p_parse p_exp
 <?> "p_bin_op_exp_inner"  p_exp :: Parser Exp p_exp =  listplus [p_const_exp, p_bin_op_exp]         <?> "p_exp"


到目前为止,我们的parser部分就完工了。

 

对Haskell有兴趣的同学,可以安装下ghci,是haskell的REPL,然后加载刚才写好的Parser.hs,在命令行里试一下:

 

Prelude> parse p_exp "" "(mul (sub 5 (add 1 2)) 4)"

 

可以看到输出结果。稍微排版下,输出结果变成了我们熟悉的树形结构,Op为「mul」的BinOpExp就是树的根节点。整个输出就是一棵AST。

 

Right (BinOpExp 
            "mul" 
            (BinOpExp 
                "sub" 
                (ConstExp (IntVal 5)) 
                (BinOpExp 
                    "add" 
                    (ConstExp (IntVal 1)) 
                    (ConstExp (IntVal 2)))) 
            (ConstExp (IntVal 4)))

 

有了这棵AST,我们就可以开始做后续的代码生成了。

 

CodeGenerator的主体是把Exp转换成目标语言代码的函数:

 

exp_gen :: Exp -> Maybe String
exp_gen (BinOpExp op e1 e2) = do
    s1 <- exp_gen e1
    sop <- op_gen op
    s2 <- exp_gen e2
    return (format "({0} {1} {2})" [s1, sop, s2])
exp_gen (ConstExp val) = do
    case val of 
        IntVal int_val -> return (show int_val)
        FloatVal float_val -> return (show float_val)

 

利用模式匹配这个语言特性实现多态既容易又优雅。

 

最后再套个壳,比如读源文件,写目标文件,整个编译器就大功告成了。

好了,看到这里,相信你对怎么快速写一个编译器已经有了比较充分的了解。

 

当然,我不否认,「21分钟学会写编译器」有标题党的嫌疑,如果想按本文介绍的方法从零开始写编译器,即使不用学flex和bison,不用回忆编译原理课程内容,也还是需要了解PEG,了解自己熟悉语言的ParseC库用法。

 

但是,这仍然是个人认为的最快上手写编译器的方法。远离痛苦的抽象概念,从动手开始,不正是很多同学喜欢上写代码的初心吗?

 

如之前所说,我们虽然实现了一个编译器,但是这个编译器非常蠢,比如BinOp的存在本身就有问题:

 

  • BinOp在AST中用字符串表示,我们就没办法检查两个操作数的类型。
  • BinOp成了特殊概念,而不是普通的函数。

 

文章中的一些背景知识由于篇幅原因我没办法一一解释,这里简单列个reference,对相关话题感兴趣的可以去搜索引擎搜索对应的关键字:

 

  • haskell的相关概念,看real world haskell即可。
  • ParseC的相关概念,可以找这几篇文献,「the essence of fp」「monads for fp」「monadic parser combinator」。
  • 编译原理相关概念,不建议看《龙书》,有兴趣的可以翻翻看《Engineering a Compiler》。

 

 

原文:21分钟学会写编译器

本文来自网易云社区,经作者王迅授权发布。

 

网易云新用户大礼包:https://www.163yun.com/gift

更多网易研发、产品、运营经验分享请访问网易云社区。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

21分钟学会写编译器 的相关文章

  • freeswitch系列六 freeswitch在拨号计划中通过lua实现对redis操作

    3种freeswitch访问redis方案的分析 由于项目的原因 xff0c 需要在freeswitch的拨号计划中根据redis中特定key的值 xff0c 判断后续的操作是转发请求或者播放录音 这里需要freeswitch中实现对red
  • 接口异常状态统一处理方案在 Firefox 下无效的原因和解决方案

    没想到会是在双十一这么忙的时间段把这篇文章写完 xff0c 公司很忙很紧张 xff0c 可我还有时间在公司做分享 xff0c 写博文 xff0c 惭愧惭愧 做后台系统在双十一期间不如 2c 端的小伙伴有参与感呀 问题根源 上文 接口异常状态
  • 玩转神龙服务器的Hyper-V虚拟化网络之 配置直通网卡

    在上一篇 玩转神龙服务器的Hyper V虚拟化网络之 配置NAT网络 的文章中我们使用NAT的方式使Hyper V VM可以访问公网 在这一篇里 xff0c 我们会使用直通网卡的特性来使VM有对外提供服务的功能 前置条件 神龙服务器 xff
  • postgresql学习笔记1---安装和psql基本操作

    本文是PostgreSQL修炼之道这本书的学习笔记 xff0c 记录下疑惑或不解的地方 xff0e 这里也列一些资源 官方文档 http www postgresql org files documentation pdf 9 4 post
  • python 在字典中添加键值对的方法。

    list 添加元素的方法是 list append xff08 a xff09 将 a 添加到 list 里 dict 添加元素的方法是 dict update dict2 意为 xff0c 将 dict2 的内容添加到 dict 中 转载
  • 常用的4种开发模式

    常用的4种开发模式 1 瀑布式开发 瀑布式开发是由W W Royce在1970年提出的软件开发模型 xff0c 是一种比较老的计算机软件开发模式 xff0c 也是典型的预见性的开发模式 在瀑布式开发模式中 xff0c 开发严格遵循预先计划的
  • 骚猪队的模板

    SaoZhu Team Code Library 2017 11 TAGS ACM for newest edition click here East China Normal University Chen WeiWen Softwar
  • 用nodejs库cheerio抓取网页内容与图片

    之前都是PHP phpQuery 抓取 xff0c 但jQuery更强大 xff0c 于是用nodejs 只是node jquery的依赖太多 xff0c 只好用cheerio 下面是一个抓取脚本 xff1a var http 61 req
  • 完整的系统帮助类Utils

    来源 xff1a http www cnblogs com yuangang p 5477324 html using System using System Collections Generic using System Linq us
  • 转载--git教程

    http lazynight me 2898 html 转载于 https www cnblogs com benchan2015 p 4897797 html
  • 网络通信第一课 C++封装HTTP请求报文说明

    一个HTTP请求报文由请求行 xff08 request line xff09 请求头部 xff08 header xff09 空行和请求数据4个部分组成 使用C 43 43 组装上述报文 boost asio streambuf requ
  • [重要新功能]删除自己发表的评论

    当你登录后 使用cookie也可以 发表评论 不管是使用普通评论还是高级评论 xff0c 你就可以在其他人的Blog中删除自己发表过的评论 这样你在发表评论时 xff0c 如果写错了内容 可以删除后重发 接着 xff0c 准备增加在管理页面
  • 解析FAT16文件系统

    引导扇区的信息例如以下 xff1a 1 偏移地址00H xff0c 长度3 xff0c 内容 xff1a EB 3C 90 跳转指令 2 偏移地址03H xff0c 长度8 内容 xff1a 4D 53 44 4F 53 35 2E 30
  • 将 n个球放入M个盒子中, 设每个球落入各个盒子是等可能的,求有球的盒子数X 的期望...

    将 n个球放入M个盒子中 设每个球落入各个盒子是等可能的 求有球的盒子数X 的期望 引入随机变量 xi 表示第i个盒子有没有球 则 X 61 X1 43 X2 43 43 XM 于是 E X 61 E X1 43 E X2 43 43 E
  • Navicat for MySQL Mac 破解版

    今天在macOS 系统下搭建 Java开发环境 xff0c 需要配置MySQL xff0c 按照Windows的习惯 xff0c 使用Navicat for MySQL 操作比较习惯 然后找不到比较好的破解版 xff0c 这里介绍一个老版的
  • Echarts中X轴只显示最大值和最小值

    目标 xff1a 本篇文章是介绍使用Echarts时设置X轴上的刻度只显示最大值和最小值 xff0c 不显示其他的刻度 这个我在做项目的过程中遇到的一个需求 xff0c 我花费了很长的时间才找到的一种解决办法 xff0c 希望对后面遇到此坑
  • 机器学习期中考复习(md全是证明题)

    佛了
  • 页面字体随窗口变化大小

    详细描述 遇到了一个手机页面字体不能定死的问题 xff0c 页面会随着页面改变 xff0c 而改变大小 师弟遇到的问题 xff0c 我也遇到过 xff0c 我感觉这个东西可能还会有人遇到 截图 分辨是1000px的字体大小和400px的字体
  • 从Hadoop URL中读取数据

    为什么80 的码农都做不了架构师 xff1f gt gt gt 要从Hadoop文件系统中读取文件 xff0c 一个最简单的方法是使用java net URL对象来打开一个数据流 xff0c 从而从中读取数据 一般的格式如下 xff1a 1
  • 【01月11日】【精彩电影合集】【10部】【亲测】【Lsyq5647发布】

    今日电影更新 10部 1 爱情 07最新动作大片DVD中字 2 国家宝藏2 xff1a 神秘书 美国2008 动作大片DVD中文字幕 3 龙过鼠年 范伟 赵本山2008贺岁大片国语DVD版 4 本能 沙郎斯通性感演绎DVD未删减版 5 黑水

随机推荐

  • 关于ElasticsearchRepository的使用笔记

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 一个很全的API链接文档 以下是使用Spring data Jpa操作ES的一些记录 在ElasticsearchRepository中我们可以使用Not Add Like
  • Java 判断实体类属性是否为空工具类

    2019独角兽企业重金招聘Python工程师标准 gt gt gt import org apache commons lang StringUtils import java lang reflect Field import java
  • TLS Error: TLS handshake failed解决办法

    直接修改端口号 服务器端和客服端都要改哟 转载于 https blog 51cto com luoguoling 1080298
  • windows禁用rc4 算法

    公司的Windows服务器被扫描出安全漏洞 SSL TLS 受诫礼 BAR MITZVAH 漏洞 CVE 2015 2808 和安全厂家沟通 xff0c 漏洞是由rc4算法 xff0c 引起的 xff01 把服务里面的rc4算法禁用就行了
  • iOS 抓取 UIwebview 上 所有 图片 并进行滚动播放

    关于在UIwebview上添加滚动图片 两种滚动手势会混淆 xff0c 应为webview有webview scrollview的属性 故参照昨天的随笔 scrollview嵌套解决方案 本篇随笔主要讲循环使用正则表达式 xff0c 本人在
  • 优化网络爬虫

    Date 2019 07 03 Author Sun 优化之前的网络爬虫代码如下 xff1a code coding utf 8 author 61 39 sun 39 date 61 39 2019 7 3 上午10 53 39 from
  • 如何使用SendMessage发送按键组合,例如:Ctrl+A

    代码 var hwnd Integer begin hwnd 61 FindWindow 39 Notepad 39 nil SetForegroundWindow hwnd keybd event VK CONTROL MapVirtua
  • 算法题:按规律输出

    编写算法 xff1a 打印具有下面规律的图形 1 5 2 8 6 3 10 9 7 4 输入 xff1a 手动输入n 输出 xff1a 格式输出n行 思路 xff1a 1 定义100x100的二维数组并给其赋值 a30a31a32a33a2
  • Python聚类色彩提取——Scipy-kmeans

    一 聚类 xff1a 物以类聚 数组可以进行聚类 xff0c 并找到数组的聚类中心 使用的第三方库是scipy xff0c 需要pip install scipy xff0c 先安装该库 数组聚类代码 xff1a import numpy
  • 推荐 3 款实用 Node.js 版本管理工具

    为了能够对 Node js 版本进行版本管理 xff0c 我整理了 3 款非常实用的 Node js 版本管理工具 xff0c 让大家能够自由地切换本地环境不同的 Node js 版本 1 nvm Github stars 60K 43 n
  • ipv6 neutron应用(一)

    一 neutron支持ipv6 xff0c 有2个重要的属性 1 ipv6 ra mode 2 ipv6 address mode 这2个属性都可以设置下面三个值 1 slaac 2 dhcpv6 stateful 3 dhcpv6 sta
  • 理解Compressed Sparse Column Format (CSC)

    最近在看 Spark for Data Science 这本书 xff0c 阅读到 Machine Learning 这一节的时候被稀疏矩阵的存储格式CSC给弄的晕头转向的 所以专门写一篇文章记录一下我对这种格式的理解 目的 Compres
  • FBOSS

    https github com facebook fboss
  • 五款针对Ubuntu系统的最佳杀毒软件

    随着使用 Linux 作为主要桌面的用户越来越多 xff0c 很多黑手都伸向了这部分用户 虽然目前专门针对 Linux 的专有恶意软件还比较少 xff0c 但大家还是需要保持相当的谨慎才是才策 由于大部分 Linux 新手用户中 Ubunt
  • 解决www.github.com访问太慢的问题

    解决www github com访问太慢的问题 使用www github com的过程中 xff0c 有时候打开会特别的慢 xff0c 原因github com的域名被一堵伟大的墙挡在了外面 但是我们可以通过修改本机的hosts文件来修改这
  • 国内有哪些好的刷题网站?

    http www zhihu com question 25574458 Luau Lawrence xff0c Data Mining 弱鸡 PhD 64 NTU 温梦强 石一帆 知乎用户 等人赞同 Welcome To PKU Judg
  • 使用update命令来修改Mysql的root密码

    1 xff0c 使用update命令来修改Mysql的root密码 使用Mysql update命令既可以修改root的老密码 xff0c 也可设置root的密码为空 xff0c 如果使用update命令更改root的密码 xff0c 需要
  • 设置 java -jar 的进程显示名称

    有时候我们会用 nohup java jar xxx jar 来将一些可执行的java application挂在后台 xff0c 类似windows服务一样来运行 但是有一个不爽的地方 xff0c 在linux终端里用jps 显示时 xf
  • linux下修改hostid

    linux下修改hostid 网上有很多版本 xff0c 总结了这几点 1 gt 一个以16进制显示的int字符串 xff1b 2 gt 配置文件 etc hostid 如果有值 xff0c 输出 xff0c 结束 3 gt 从hostna
  • 21分钟学会写编译器

    本文来自网易云社区 知乎上有一种说法是 编译器 图形学 操作系统是程序员的三大浪漫 先不管这个说法是对是错 xff0c 我们假设一个程序员在国内互联网公司写代码 xff0c 业余时间不看相关书籍 那么三年之后 xff0c 他的这些知识会比在