惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Cloudflare Blog
N
News and Events Feed by Topic
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
AWS News Blog
AWS News Blog
S
SegmentFault 最新的问题
T
Tailwind CSS Blog
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Spread Privacy
Spread Privacy
J
Java Code Geeks
博客园 - 聂微东
T
Tor Project blog
宝玉的分享
宝玉的分享
博客园 - 叶小钗
Webroot Blog
Webroot Blog
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Heimdal Security Blog
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
I
InfoQ
Security Latest
Security Latest
Martin Fowler
Martin Fowler
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy International News Feed
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
H
Help Net Security
L
LINUX DO - 最新话题
L
LINUX DO - 热门话题

Mereith's Blog

记一次代理转发问题的排查与解决 PVE DataCenter Manager 端口号跳转错误修复 k8s + jenkins + gitlab 触发器 【转载】没有编程生产力这样的东西 【后续】居然被 ddos 了 居然被 ddos 了 Tagger - 让版本标签管理更简单 openclash 开启后端口转发失效 nextjs 启动时执行代码 买了太多VPS不知道干什么,干脆做个网站 juhost Level-1 测评 VMISS CN - Hong Kong VMISS JP - Tokyo - BGP 测评 VMISS CN - Hong Kong - BGP HomeLab 的终点是 最近在做的事情 因为选择艰难症,自己写了一套开源博客系统 手撸一个nodejs分布式爬虫,还要可视化 HomeLab 分享 我的个人工作流——开源项目推荐 safari 插件开发 Safari Extensions Preferences 按钮无响应 设置移动端软键盘回车按钮文案 nginx proxy manager 非标准端口反代 Host 不对 监控 k8s ingress 自动添加域名 DNS 解析 使用 ingress 注解给 traefik ingress 添加中间件 使用流水线功能为文章添加固定结尾 为 VanBlog 添加一个小挂件 基于React/umi/egg自建博客系统 如何正确停止 NodeJS 子进程 k8s pod 替换策略 让威联通、pve 共享 ups 实现断电关机 Ubuntu 配置 samba 混合云部署k3s集群(基于wireguard) pve 无法启动 grub lvmid not found error antd Password 关闭自动补全 code-server node not found pve no quorum 错误 || 无法登录 pve Cannot Initiate CMAP Service pve 节点删不干净 PVE 指针(鼠标)漂移 pve 重启网络 Proxmox VE 直通显卡 ProxmoxVE (PVE) 7.0 换源升级 用 SOCAT 端口转发 git 无法 pull 仓库refusing to merge unrelated histories 用 vuepress 搭建私人知识库 js 防抖节流中的 this 与箭头函数 Dockerfile 给 Ubuntu 换源 docker 内访问宿主机 docker 删除无用镜像 css 实现展开收起动画 nginx 301 问题 Node.js 流式编程 关于我重写了三次博客项目这件事 css flex 布局超出隐藏 css 隐藏滚动条 css 文字超出隐藏并显示省略号 css 填充 svg 颜色 css 黑白滤镜 nginx 反代丢端口 docker-compose 部署 matomo 分析系统 基于 strapi 开发应用 strapi 关闭 Content Security Policy nginx 反代 rewite url ssh 通过跳板机访问目标机器 Cent os 7 安装 zsh 5.6 以上版本 win11 任务栏不合并 bash 数组操作 vim 修改文件格式为 unix k8s 集群 control-plane-endpoint 主机名 git https 保存密码 Bash 判断命令存在 & 重定向 python 导出项目依赖 cent os 7 安装 gcc 版本 9 System limit for number of file watchers reached cent os 7 安装 git 2.x css 下划线中间向两边滑动效果 linux 挂载 SMB/CIFS linux 清除 ACL windows 命令行安装 inf 驱动 css 文字不可选择 iperf3 网络测速工具 cent os 换源 k8s 1.21.3 从 docker 迁移到 containerd 解决Nginx安装错误:No package nginx available 问题 从pve的硬盘里导出数据 Errors were encountered while processing adguardhome配合clash pve中使用LXC容器安装OMV LXC直通硬盘 nginx反代模版 docker常用开启容器的命令 PVE的LXC容器中安装Docker ProxmoxVE系统分区相关 基于clash搭建旁路网关 Portainer添加节点 linux声卡设置及录音音 OpenWrt用VLAN PVE换源及去掉订阅提示和改端口 adguardhome踩坑
动手写一个超简单的编译器
wanglu@mereith.com (mereith) · 2023-06-28 · via Mereith's Blog

请注意,本文编写于 1074 天前,最后修改于 1074 天前,其中某些信息可能已经过时。

不知不觉已经写了两年代码了,每过一阵子回头看看原来的代码,都觉得有不少值得改进的地方,这真是一件高兴的事。
原来我从来不会关心编译原理这种东西,感觉离自己很远,但做项目的过程中,操作了一些 AST,杂七杂八的接触过一些相关的知识,又发现了一个很好的教程,就跟着写了一遍,颇有收获。
在此把大概的步骤和思路记下来。

教程: Create Your Own Compiler
代码: https://github.com/Mereithhh/simple-compiler

目标

我们的目标是用 js 写一个编译器,把 lisp 编译成 js 代码。


可以编译成下面的 js 代码:


用起来可能是这样的:

const compiler = require("./compiler"); const input = "(add 123 ( sub 4 3))" const output = compiler(input); // add(123, sub(4, 3)); const add = (a,b) => a + b; const sub = (a, b) => a - b; const result = eval(output) console.log(result) // 124

概述

要实现这样一个编译器,大概要 4 步:

  • 词法分析
  • 语法分析
  • AST转换
  • 代码生成

我们一步一步来。

词法分析

这步的目的是解析我们传入代码字符串中的有效单词,并给他们分类。我们的输入是:


这步完成后,就会输出这样的数组:

[ { "type": "paren", "value": "(" }, { "type": "name", "value": "add" }, { "type": "number", "value": "123" }, { "type": "paren", "value": "(" }, { "type": "name", "value": "sub" }, { "type": "number", "value": "4" }, { "type": "number", "value": "3" }, { "type": "paren", "value": ")" }, { "type": "paren", "value": ")" } ]

要想实现这样的效果,我们只需要简单从头到尾捋一遍我们的输入,根据不同的情况判断就好了,大概是这样的:

const PATTERNS = [ "(", ")" ] const NUMBER = /[0-9]/; const LETTER = /[a-zA-Z]/; const SPACE = /\s/; module.exports = function tokenizer(input) { const tokens = []; let current = 0; while (current < input.length) { let char = input[current]; if (PATTERNS.includes(char)) { tokens.push({ type: "paren", value: char, }); current++; continue; } if (NUMBER.test(char)) { let value = ""; while (NUMBER.test(char)) { value += char; char = input[++current]; } tokens.push({ type: "number", value, }); continue; } if (LETTER.test(char)) { let value = ""; while (LETTER.test(char)) { value += char; char = input[++current]; } tokens.push({ type: "name", value, }); continue; } if (SPACE.test(char)) { current++; continue; } throw new Error(`unknow charactor: ${char}`) } return tokens; }

语法分析

语法分析是把我们的上一步获得的词组,重新组合成树状的结构,也就是 AST。 处理好之后,会得到这样的树:

{ "type": "Program", "body": [ { "type": "CallExpression", "name": "add", "params": [ { "type": "NumberLiteral", "value": "123" }, { "type": "CallExpression", "name": "sub", "params": [ { "type": "NumberLiteral", "value": "4" }, { "type": "NumberLiteral", "value": "3" } ] } ] } ] }

这一步实现的核心思路是递归,定义一个当前下标的变量,和一个递归执行的函数。 在递归执行的函数里分别处理不同类型的数据,如果遇到了符号类型的数据,那就递归调用这个函数。不废话直接上代码:

module.exports = function parser(tokens) { let current = 0; // walk 函数每次运行完,都增加一个指针,以被下一次 walk function walk() { let token = tokens[current]; if (token.type === 'number') { current++; return { type: 'NumberLiteral', value: token.value, } } if (token.type === 'paren' && token.value === '(') { // 下一个 token 才是函数名称 token = tokens[++current]; const expression = { type: "CallExpression", name: token.value, params: [] } token = tokens[++current]; while (token.value !== ")") { expression.params.push(walk()) token = tokens[current] } current++; return expression; } throw new TypeError(`Unknow token: ${token.type}`) } const ast = { type: 'Program', body: [walk()], }; return ast; }

AST转换

上一步我们得到了 lisp 语言得 AST 树,现在我们要把这个树转换成 js 的 AST 树。
核心思路是遍历整个树,在遍历过程中针对每一种节点类型,使用不同的函数转化成另一种树,并且要定义一个位置变量,来把转换后的节点插入到新树中适合的位置。代码如下:

const traverse = require("./traverse"); module.exports = function transformer(originalAST) { const jsAST = { type: 'Program', body: [], }; let position = jsAST.body; traverse(originalAST, { NumberLiteral(node) { position.push({ type: "NumericLiteral", value: node.value, }) }, CallExpression(node, parent) { let expression = { type: "CallExpression", callee: { type: "Identifier", name: node.name }, arguments: [] }; const prevPosition = position; position = expression.arguments; if (parent.type !== "CallExpression") { expression = { type: "ExpressionStatement", expression, }; }; prevPosition.push(expression) } }) return jsAST; }

其中遍历函数的实现如下:

module.exports = function traverse(ast, visitors) { function walkNode(node, parent) { const method = visitors[node.type]; if (method) method(node, parent); if (node.type === "Program") walkNodes(node.body, node); else if (node.type === "CallExpression") walkNodes(node.params, node); } function walkNodes(nodes, parent) { nodes.forEach(node => walkNode(node, parent)) } walkNode(ast, null) }

代码生成

这步很简单,只要把我们需要的 jsAST 生成成相应的代码就好了,因为我们的编译器支持的语法非常简单(简陋),所以代码也很简洁🫤

module.exports = function generateCode(node) { if (node.type === "NumericLiteral") { return node.value; } if (node.type === "Identifier") { return node.name; } if (node.type === "CallExpression") { // name(arg1, arg2, arg3) return `${generateCode(node.callee)}(${node.arguments.map(generateCode).join(", ")})` } if (node.type === "ExpressionStatement") { return `${generateCode(node.expression)};` } if (node.type === "Program") { return node.body.map(generateCode).join("\n") } }

总结

跟着教程走下来,发现编译器(入门)并没有我想的那么复杂,写一个最基础的编译器并不需要多高深的算法,代码也没有多难懂。 简洁的代码又非常巧妙,很少的代码实现了需要的功能。 其中递归是个很重要的应用,合理的利用递归可以写出有些“魔力”的代码。

实际上在平时的开发中,我发现“抽象”是一种非常重要的能力,一个会合理抽象的程序员,可以用很少的代码完成很强的功能,同时具有很强的扩展性和鲁棒性。

之前我看过“计算机程序的构造与解释”这门网课,结合今天的代码又有了些新的思考,很推荐这门课,有空我应该会再看一遍👀