关于编译原理:手写编程语言如何为-GScript-编写标准库

版本更新最近 GScript 更新了 v0.0.11 版本,重点更新了: Docker 运行环境新增了 byte 原始类型新增了一些字符串规范库 Strings/StringBuilder数组切片语法:int[] b = a[1: len(a)];具体更新内容请看下文。前言前段时间公布了 GScript 的在线 playground, 这是一个能够在线运行 GScript 脚本的网站,其本质原理是接管用户的输出源码从而在服务器上运行的服务;这几乎就是后门大开的 XSS 攻打,为保住服务器我设置了运行 API 的后端服务的用户权限,这样能够防止执行一些歹意的申请。 但也防止不了一些用户执行了一些耗时操作,比方一个死循环、或者是我提供 demo 里的打印杨辉三角。 这实质上是一个递归函数,当打印的三角层数过高时便会十分耗时,同时也十分耗费 CPU。 有几次我去查看服务器时发现了几个 CPU 过高的过程,基本上都是这样的耗时操作,不可避免的会影响到服务器的性能。 应用 Docker为了解决这类问题,很天然的就能想到能够应用 Docker,所有的资源都和宿主机是隔离开的,无论怎么瞎折腾也不会影响到宿主机。 说干就干,最初批改了 API 执行脚本的中央: string fileName = d.unix("Asia/Shanghai") + "temp.gs" ; s.writeFile(fileName, body, 438); string pwd = s.getwd(); // string res = s.command("gscript", fileName); string res = s.command("docker","run","--rm","-v", pwd+":/usr/src/gscript","-w","/usr/src/gscript", "crossoverjie/gscript","gscript", fileName); s.remove(fileName); r.body = res; r.ast = dumpAST(body); r.symbol=dumpSymbol(body); ctx.JSON(200, r);次要批改的就是将间接执行的 GScript 命令批改为了调用 docker 执行。 ...

October 17, 2022 · 3 min · jiezi

关于编译原理:手写编程语言递归函数是如何实现的

前言本篇文章次要是记录一下在 GScript 中实现递归调用时所遇到的坑,相似的问题在中文互联网上我简直没有找到相干的内容,所以还是很有必要记录一下。 在开始之前还是简略介绍下本次更新的 GScript v0.0.9 所蕴含的内容: 反对可变参数优化 append 函数语义优化编译错误信息最初一个就是反对递归调用先看第一个可变参数: //formats according to a format specifier and writes to standard output.printf(string format, any ...a){}//formats according to a format specifier and returns the resulting string.string sprintf(string format, any ...a){}以上是随着本次更新新增的两个规范函数,均反对可变参数,其中应用 ... 示意可变参数,调用时如下: printf("hello %s ","123");printf("hello-%s-%s ","123","abc");printf("hello-%s-%d ","123",123);string format = "this is %s ";printf(format, "gscript");string s = sprintf("nice to meet %s", "you");assertEqual(s,"nice to meet you");与大部分语言相似,可变参数实质上就是一个数组,所以能够拿来循环遍历: int add(string s, int ...num){ println(s); int sum = 0; for(int i=0;i<len(num);i++){ int v = num[i]; sum = sum+v; } return sum;}int x = add("abc", 1,2,3,4);println(x);assertEqual(x, 10);// appends "v" to the end of a array "a"append(any[] a, any v){}之后是优化了内置函数 append() 的语义,本次优化来自于 issue12 的倡议:https://github.com/crossoverJie/gscript/issues/12 ...

September 27, 2022 · 3 min · jiezi

关于编译原理:里程碑用自己的编程语言实现了一个网站

前言在上一篇《终于实现了一门属于本人的编程语言》 介绍了本人写的编程语言 GScript ,在文中提到心愿最终能够应用 GScript 开发一个网站。 到目前为止的确是做到了,首页地址: https://gscript.crossoverjie.top/index 要称为一个网站的确有点勉强,不过也是一个动静网页,因为返回的是 HTML,所以在以后阶段只有不嫌麻烦其实也能写一个“合格”的网站,有点像以前咱们学习 Java 时的 servlet。 该页面的源码地址在这里:https://github.com/crossoverjie/gscript-homepage 其实总共也就40来行代码: class GScript{ string author; string[] features; string since; GScript(string a, string[] f, string s){ author = a; features = f; since = s; }}func (HttpContext) index(HttpContext ctx){ string[] features = {"statically", "strongly"}; GScript gs = GScript("crossoverJie",features, "2022"); string j = JSON(gs); println(j); string local = getCurrentTime("Asia/Shanghai","2006-01-02 15:04:05"); println("local=" + local); string html = ^ <html> <title>GScript</title> <pre> _ _ ___ ___ ___ ___|_|___| |_ | . |_ -| _| _| | . | _||_ |___|___|_| |_| _|_| |___| |_| v0.0.7 ^+ j +^ </pre> <h1>current ^+ local +^</h1> <p><a href="https://github.com/crossoverjie/gscript-homepage">GScript-homepace source code</a></p> </html> ^; ctx.HTML(200, html);}httpHandle("GET", "/index", index);string[] args = getOSArgs();if (len(args) ==3){ httpRun(":" + args[2]);}else { httpRun(":8000");}全是利用 GScript 所提供的规范库实现的,后文会具体聊聊内置 HTTP 包。 ...

September 14, 2022 · 3 min · jiezi

关于编译原理:终于实现了一门属于自己的编程语言

前言都说程序员的三大浪漫是:操作系统、编译原理、图形学;最初的图形学的确是特定的业余畛域,咱们简直接触不到,所以对我来说换成网络更适合一些,最初再加上一个数据库。 这四项技术如果都能把握的话那岂不是在 IT 行业横着走了,加上这几年互联网行业越来越不景气,越底层的技术就越不可能被代替;所以为了给本人的 30+ 危机留点前途,从往年上半年开始我就逐步开始从头学习编译原理。 功夫不负有心人,通过近一个月的挑灯夜战,每晚都在老婆的督促下才劳动,克服了中途好几次想放弃的激动,终于当初实现了 GScript 一个预览版。 预览版的意思是语法结构与整体设计根本实现,后续更新也不太会改变这部分内容、但还短少一些易用性能。个性首先来看看保留环节, GScript 是如何编写 hello world 的。 hello_world.gs: println("hello world");❯ gscript hello_world.gshello world废话说完了接下来重点聊聊 GScript 所反对的个性了。 后文会重点阐明每一个个性。 例子除了方才提到的 hello world,再来看一个也是示例代码常常演示的打印斐波那契数列。 void fib(){ int a = 0; int b = 1; int fibonacci(){ int c = a; a = b; b = a+c; return c; } return fibonacci;}func int() f = fib();for (int i = 0; i < 5; i++){ println(f());}输入后果如下: 01123整体写法与 Go 官网举荐的相似:https://go.dev/play/p/NeGuDahW2yP ...

September 7, 2022 · 3 min · jiezi

关于编译原理:几百行代码实现一个脚本解释器

前言最近又在重新学习编译原理了,其实两年前也温习过,当初是为了能实现通过 MySQL 的 DDL 生成 Python 中 sqlalchemy 的 model。 相干文章在这里:手写一个词法分析器 尽管实现了相干性能,但当初看来其实实现的比拟糙的,而且也只使用到了词法剖析;所以这次我的目标是能够通过词法剖析->语法分析->语义剖析 最终能实现一个功能完善的脚本"语言"。 成果当初也有了一些阶段性的成绩,如下图所示: 目前具备以下基本功能: 变量申明与赋值(只反对 int)二次运算(优先级反对)语法查看debug 模式,能够打印 AST感兴趣的敌人能够在这里查看源码:https://github.com/crossoverJie/gscript 本地有 go 环境的话也能够装置运行。 go get github.com/crossoverJie/gscriptgscript -h或者间接下载二进制文件运行:https://github.com/crossoverJie/gscript/releases 实现以后版本是应用 go 编写的,的确也如题目所说,外围代码还不到 1k 行代码,当然这也和目前性能简陋无关。 不过麻雀虽小五脏俱全,从以后版本还是使用到了编译原理中的局部常识:词法、语法分析。 根本实现流程如上图: 通过词法分析器将源码中解析出 token再通过对 token 推导生成出形象语法树(AST) 如果语法语法呈现谬误,这一步骤便会抛出编译失败,比方 2*(1+ 少了一个括号。因为没有应用相似于 ANTLR 这样工具来辅助生成代码(不然性能也不会只有这么点),所以其中的词法、语法分析都是手写的,代码量并不大,对于想要调试的敌人能够间接查看源码。 词法分析器:token/token.go:39语法分析器:syntax/syntax.go 其中会波及到一些概念,比方无限状态机、递归降落算法等知识点就没在本文探讨了,后续这个我的项目性能更加欠缺后也会重头整顿。 布局最初是画饼阶段了,不出意外后续会持续新增如下性能: 更多的根底类型,string/long 之类的。变量作用域、函数。甚至是闭包。OOP 必定也少不了。这些个性都实现后那也算是一个"古代"的脚本语言了,后续我也会持续更新学习和实现过程中的乏味内容。 源码地址:https://github.com/crossoverJie/gscript

May 30, 2022 · 1 min · jiezi

关于编译原理:编译原理与设计-41-自上而下分析法

自上而下分析法语法树的从左到右叶结点=#,则 #∈L(G)。 1. 文法的逐级优化打消左递归 含有A→Aa模式产生式的文法:间接左递归两步或两步以上:间接左递归打消形式: 写正规式→转化为右递归间接:先代入提取左公因子通过改写产生式来推延决定预测分析法的工作过程:从文法开始符号登程,在每一步推导过程中依据以后句型的最左非终结符A和以后输出符号a,抉择正确的A-产生式。为保障剖析的确定性,选出的候选式必须是惟一的。S_文法(简略的确定性文法) 每个产生式的右部都以终结符开始同一非终结符的各个候选式的首终结符都不同留神:S_文法不含\( \varepsilon \)产生式为了退出\( \varepsilon \)产生式,须要看非终结符前面能跟什么终结符:FOLLOW集 新定义: 非终结符A的后继符号集 可能在某个句型中紧跟在A后边的终结符a的汇合,记为FOLLOW(A)产生式的可全集 产生式A→B的可全集是指能够选用该产生式进行推导时,对应的输出符号的汇合,记为SELECT(A→B) SELECT(A→aB) = {a}SELECT(A→\( \varepsilon \)) = FOLLOW(A)q_文法 每个产生式的右部或为\( \varepsilon \),或以终结符开始具备雷同左部的产生式有不相交的可全集q_文法不含右部以非终结符打头的产生式 新定义: 串首终结符集 给定一个文法符号串a,a的串首终结符集FIRST(a)被定义为,能够从a推导出的所有串首终结符形成的汇合,包含\( \varepsilon \)新 · 产生式的可全集 产生式A→B的可全集是指能够选用该产生式进行推导时,对应的输出符号的汇合,记为SELECT(A→B) 如果\( \varepsilon \notin FIRST(a) \),那么SELECT(A→a) = FIRST(a)如果\( \varepsilon \in FIRST(a) \),那么\( SELECT(A→a) = (FIRST(a)-\varepsilon) \cup FOLLOW(A) \)LL(1)文法名称含意 第一个“L”示意从左向右扫描输出第二个“L”示意产生最左推导“1”示意在每一步中只须要向前看一个输出符号来决定语法分析动作2. FIRST集和FOLLOW集的计算每一个都是循环应用,直到不再增大 FIRST集 a. 找右侧第一个为终结符或空串 b. 找右侧,从左至右,非终结符的FIRST集FOLLOW集

April 30, 2022 · 1 min · jiezi

关于编译原理:BITMiniCC-从跑通到跑路

须要java版本大于15 1. 下载从GitHub下载BITMiniCC压缩包,解压 2. 关上右击文件夹,抉择用IDEA关上 抉择Trust Project 3. 更改编码抉择GBK点击convert抉择UTF-8点击convert 4. 设置我的项目构造工具栏File中抉择Project Structure,抉择Modules点击图中的×,删除默认的我的项目构造点击减少根目录抉择BITMiniCC文件夹,显示应与下图雷同 5. 设置我的项目库抉择Libraies,点击+号抉择lib文件夹,点击确定点击确定,后果如图所示 6. 设置main函数配置参数点击run中的edit configuration将框中内容改为你要剖析的C文件门路(图中为scanner的test文件) 7. 剖析config.xml配置文件 skip= 示意是否跳过,true为不运行该步,图中仅运行词法剖析scanpath= 剖析代码的门路,空字符串为默认门路,前面本人写词法剖析后这里要改 批改后功败垂成,间接run就能够后果:词法剖析后果:

April 11, 2022 · 1 min · jiezi

关于编译原理:编译原理-正规式运算四个特例理解

1. 先验常识设∑为无限字母表,在∑上的正规式与正规集可递归定义如下: 和是∑上的正规式,它们示意的正规集别离为{}和;对任何a∈∑, a是∑上的正规式,它的正规集为{a};若 r,s 都是正规式 , 它们的正规集别离为R和S , 则(r|s)、(r·s)、(r)*也是正规式,它们别离示意的正规集是:R∪S,RS,R*。此处重点为 正规式示意的正规集为{}正规式示意的正规集为正规式(r|s)示意的正规集为R∪S正规式(r·s)示意的正规集为RS正规式(r)*示意的正规集为R*其中 正规集RS应用的是汇合的乘积运算,定义如下: 因而, 下文特例中正规式运算将用正规集运算, 形象化解释 2. 注释·A汇合运算为{}A=A因而·A=AØ·A汇合运算为ØA=Ø因而Ø·A=Ø|A汇合运算为{}∪A设A={a1,a2,a3...} 若a1,a2,a3...≠, {}∪A={,a1,a2,a3...}若a1=, {}∪A={a1,a2,a3...}因而|A后果为正规式A', 正规式A'的正规集为{}∪A Ø|A汇合运算为Ø∪A=A因而Ø|A=A

April 5, 2022 · 1 min · jiezi

关于编译原理:编译原理与设计-2-词法分析

预处理:转换为字符串或字符 词法剖析1. 基本功能1.1 词法规定语言因素:语法(语言的形容规定)、语义(语言的含意)巴科斯-诺尔范式BNF[元语言符号] <>:→(::=):示意“定义为”或“由……组合成”|: “或”字符与字符串 字母表符号串: *中的元素 符号串长度||前缀、真前缀后缀、真后缀子符号串(子串)1.2 定义:正规式递归定义: 构造方法 设∑为无限字母表,在∑上的正规式与正规集可递归定义如下 和是∑上的正规式,它们示意的正规集别离为{}和 对任何a∈∑, a是∑上的正规式,它的正规集为{a} 若r,s都是正规式 , 它们的正规集别离为R和S , 则(r|s)、(r·s)、(r)也是正规式,它们别离示意的正规集是:R∪S,RS,R 无限次应用上述三条规定形成的表达式,称为∑上的正规式,仅由这些正规式示意的汇合为正规集 正规式:汇合符号,正规集:汇合正规式的运算 字母表→根本正则式→递归正则式根本正则式: 字母自身 正规式:词法的示意办法确定无限状态机:词法的识别方法

March 30, 2022 · 1 min · jiezi

关于编译原理:编译原理与设计-12-编译器介绍

编译器1. 编译程序的示意须要体现编译程序的三要素: 目标语言 T宿主语言 C源语言 S函数示意T = C(S)T型图示意梯形图只有单梯形图和三梯形图符号示意 $$C_{宿}^{源目}$$

March 30, 2022 · 1 min · jiezi

关于编译原理:编译原理与设计-11-编程语言

1. 动静类型与动态类型参考: https://zhuanlan.zhihu.com/p/... 动态类型变量的类型必须先申明,即在创立的那一刻就曾经确定好变量的类型,而后的应用中,你只能将这一指定类型的数据赋值给变量。如果强行将其余不相干类型的数据赋值给它,就会引发谬误。 在编译阶段实现数据类型的相容性查看 动静类型将什么类型的数据赋值给变量,这个变量就是什么类型 在运行阶段实现数据类型的相容性查看 动静类型语言举例: PHPRubyPython动态类型语言举例: CC++JAVAC#2. 强类型与弱类型参考:https://zhuanlan.zhihu.com/p/... 强类型语言(类型不平安语言)强类型语言是一种强制类型定义的语言,即一旦某一个变量被定义类型,如果不经强制转换,那么它永远就是该数据类型。弱类型语言(类型平安语言)弱类型语言是一种弱类型定义的语言,某一个变量被定义类型,该变量能够依据环境变动主动进行转换,不须要通过现行强制转换。强类型语言举例: JavaC++Python弱类型语言举例: VBPHPJavaScript补充:隐式类型转换两种模式的隐式类型转换: 相干类型之间隐式转换如:一个int类型的数据与一个float类型的数据相加不相干类型之隐式间转换如:一个int类型数据与一个字符串类型数据相加

March 28, 2022 · 1 min · jiezi

关于编译原理:elixir-0084-关于-DFA确定性有限自动机的那些事儿

最近在看 编译原理 这本书,感觉是很棒的入门书(指难度由浅入深深深深)。前两章次要是一些概念性的货色,第三章就开始动真格的,上代码上公式了。不本人实现一下,根本就是看得云里雾里的。所以接下来一段时间可能会不定期地更新一些对于我在 编译原理 这本书里看到的货色的实现的文章。 书本的第三章介绍了 DFA 是如何对字符串进行匹配的,例如,正则表达式 (a|b)*abb 能够转换为以下的 DFA 代码。通过状态机的机制在读取字符时切换以后状态,并依据最初的状态来确定匹配是否胜利。 defmodule DFA do def init(string) do %{ s: 0, chars: String.to_charlist(string) } end def run(%{s: s, chars: [h | t]}) do s = move(s, h) run(%{s: s, chars: t}) end def run(%{s: s, chars: []}) do if s in f() do :yes else :no end end defp move(0, ?a), do: 1 defp move(0, ?b), do: 0 defp move(1, ?a), do: 1 defp move(1, ?b), do: 2 defp move(2, ?a), do: 1 defp move(2, ?b), do: 3 defp move(3, ?a), do: 1 defp move(3, ?b), do: 0 defp f(), do: [3]end这个 DFA 只会匹配到以 "abb" 结尾的字符串: ...

February 9, 2022 · 1 min · jiezi

关于编译原理:编译原理之美1-前端技术

一、参考编译原理 学习系列目录——更新ing 了解代码:编译器的前端技术 二、编译器的前后端 前端 —— 编译器对程序代码的剖析和了解过程,通常只和语言的语法无关,跟指标机器没有关系 后端 —— 生成指标代码的过程,和指标机器无关 三、词法剖析

September 28, 2021 · 1 min · jiezi

关于编译原理:编译原理之美1-概览

一、参考编译原理 学习系列目录——更新ing 了解代码:编译器的前端技术 二、

July 13, 2021 · 1 min · jiezi

关于编译原理:编译原理-学习系列目录更新ing

一、编译原理之美学习一、参考操作系统 学习系列目录——更新ing 设置工作模式与环境(上):建设计算机

July 13, 2021 · 1 min · jiezi

关于编译原理:编译原理之前端

前端:编译器对程序代码的剖析和了解过程。    词法剖析: lexical analysis,分词            实现原理:无限状态机 ,如lex GNUlex    语法分析: 依据语法规定生成程序的语法结构(形象语法数AST)             递归降落办法,Yacc、 JavaCC 、GNU Bison 、Antlr    语义剖析: 上下文剖析 打消歧义            * 变量援用消解、作用域\~\~\~\~            * 合法性检查            * 数据类型标识            * 语义剖析的某些后果,会作为属性标注在AST上后端:生成指标代码的过程,和指标机器相干。

March 6, 2021 · 1 min · jiezi

关于编译原理:编译原理AST

AST:形象语法树(abstract syntax code,AST)是源代码的形象语法结构的树状示意,树上的每个节点都示意源代码中的一种构造,这所以说是形象的,是因为形象语法树并不会示意出实在语法呈现的每一个细节,比如说,嵌套括号被隐含在树的构造中,并没有以节点的模式出现。咱们将源代码转化为AST后,能够对AST做很多的操作,包含一些你想不到的操作,这些操作实现了各种各样不拘一格的性能,给你带进一个不一样的世界。懂编译原理的确能够随心所欲。 Most compilers break down into three primary stages: Parsing, Transformation, and Code GenerationParsing is taking raw code and turning it into a more abstract representation of the code.Transformation takes this abstract representation and manipulates to do whatever the compiler wants it to.Code Generation takes the transformed representation of the code and turns it into new code.ParsingParsing typically gets broken down into two phases: Lexical Analysis and Syntactic Analysis. ...

October 13, 2020 · 2 min · jiezi

简单玩一下ASTJavaScript

直奔主题对于js,AST能干什么? babel将es6转es5mpvue、taro等将js转为小程序定制插件删除注释、console等ps: 本文只探讨AST的概念以及使用,编译原理的其他知识不做太多描述 工具库@babel/core 用来解析AST以及将AST生成代码@babel/types 构建新的AST节点前置知识 - 编译原理概述毫无疑问js是一个解释型语言,有疑问可以参考这篇文章所以这里只简单描述一下js的编译过程(大雾),有兴趣了解编译型语言详细编译过程的可以看这本 《编译原理》 词法分析:实际就是从左到右一个字一个字地扫描分解,识别出单词、符号等。例如:var num = 1会被分解成var,num,=,1语法分析将词法分析的结果,根据语法规则转化为一个嵌套的对象,也就是AST(Abstract Syntax Tree 即 抽象语法树)目标代码产生将AST转换为可执行代码生成ASTdemo.js是我随便copy来的一段代码 isLeapYear()function isLeapYear(year) { const cond1 = year % 4 == 0; //条件1:年份必须要能被4整除 const cond2 = year % 100 != 0; //条件2:年份不能是整百数 const cond3 = year % 400 ==0; //条件3:年份是400的倍数 const cond = cond1 && cond2 || cond3; console.log(cond) if(cond) { alert(year + "是闰年"); return true; } else { alert(year + "不是闰年"); return false; }}现在我要把它转成AST,这里使用@babel/core来解析,它提供了一个parse方法来将代码转化为AST。 ...

July 9, 2019 · 2 min · jiezi

编译原理实战入门用-JavaScript-写一个简单的四则运算编译器三模拟执行

现在来模拟一下 CPU 执行机器指令的情况,由于汇编代码和机器指令一一对应,所以我们可以创建一个直接执行汇编代码的模拟器。在创建模拟器前,先来讲解一下相关指令的操作。 栈在内存中,栈的特点是只能在同一端进行插入和删除的操作,即只有 push 和 pop 两种操作。 pushpush 指令的作用是将一个操作数推入栈中。 poppop 指令的作用是将一个操作数弹出栈。 addadd 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a + b,再将结果 push 到栈中。 subsub 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a - b,再将结果 push 到栈中。 mulmul 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a * b,再将结果 push 到栈中。 divsub 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a / b,再将结果 push 到栈中。 四则运算的所有指令已经讲解完毕了,是不是觉得很简单? 代码实现注意:需要引入前两篇文章词法分析和语法分析的代码 function CpuEmulator(instructions) { this.ins = instructions.split('\r\n') this.memory = [] this.re = /^(push)\s\w+/ this.execute()}CpuEmulator.prototype = { execute() { this.ins.forEach(i => { switch (i) { case 'add': this.add() break case 'sub': this.sub() break case 'mul': this.mul() break case 'div': this.div() break default: if (this.re.test(i)) { this.push(i.split(' ')[1]) } } }) }, add() { const b = this.pop() const a = this.pop() this.memory.push(a + b) }, sub() { const b = this.pop() const a = this.pop() this.memory.push(a - b) }, mul() { const b = this.pop() const a = this.pop() this.memory.push(a * b) }, div() { const b = this.pop() const a = this.pop() this.memory.push(a / b) }, push(x) { this.memory.push(parseInt(x)) }, pop() { return this.memory.pop() }, getResult() { return this.memory[0] }}const tokens = lexicalAnalysis('(100+ 10)* 10-100/ 10 +8* (4+2)')const writer = new AssemblyWriter()const parser = new Parser(tokens, writer)const instructions = parser.getInstructions()const emulator = new CpuEmulator(instructions)console.log(emulator.getResult()) // 1138编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(一)词法分析编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(二)语法分析编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(三)模拟执行编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(四)结语完整源码参考资料:计算机系统要素

June 30, 2019 · 2 min · jiezi

编译原理实战入门用-JavaScript-写一个简单的四则运算编译器四结语

四则运算编译器,虽然说功能很简单,只能编译四则运算表达式。但是编译原理前端部分几乎都有涉及,词法分析,语法分析,还有代码生成。 再复杂的编译器、再简单的编译器,功能上是差不多的,只是复杂的编译器实现上会更困难。 这个系列的文章是为了帮助你入门,在这个基础上再去看编译原理相关书籍,不至于打瞌睡。 如果你对编译原理很有兴趣,并且想更深一步的学习,在这里强烈推荐你看一本书——我心目中的神书——《计算机系统要素-从零开始构建现代计算机》。 这本书神在哪? 神在它通俗易懂,对小白足够友好,但又不过分肤浅。每一章都是理论与实践结合的经典,从计算机硬件知识到软件体系,再到编译原理和操作系统。 我在学习编译原理知识之前,看过好几本相关的书籍,无一例外,都是看得昏昏欲睡,不知所然。唯独这本书,越看越有味道,停不下来,最终我花了一个多月的时间看完了这本书并且完成了它所有的项目。 这一个多月的时间,让我有了一个质的蜕变,对于程序,不再懵懂无知。从写下一行代码开始,我就已经了解了这一个个字符最终会怎样在 CPU 中执行。 如果你在看完我的描述之后,对这本书有兴趣,欢迎你来我的项目看一下,这里有这本书的下载链接和我完成本项目的源码答案。 编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(一)词法分析编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(二)语法分析编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(三)模拟执行编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(四)结语完整源码参考资料:计算机系统要素

June 30, 2019 · 1 min · jiezi

编译原理实战入门用-JavaScript-写一个简单的四则运算编译器二语法分析

四则运算的语法规则(语法规则是分层的)x* 表示 x 出现零次或多次x | y 表示 x 或 y 将出现( ) 圆括号,用于语言构词的分组以下规则从左往右看,表示左边的表达式还能继续往下细分成右边的表达式,一直细分到不可再分为止。 expression: addExpressionaddExpression: mulExpression (op mulExpression)*mulExpression: term (op term)*term: '(' expression ')' | integerConstantop: + - * /PS: addExpression 对应 + - 表达式,mulExpression 对应 * / 表达式。 语法分析对输入的文本按照语法规则进行分析并确定其语法结构的一种过程,称为语法分析。 一般语法分析的输出为抽象语法树(AST)或语法分析树(parse tree)。但由于四则运算比较简单,所以这里采取的方案是即时地进行代码生成和错误报告,这样就不需要在内存中保存整个程序结构。 先来看看怎么分析一个四则运算表达式 1 + 2 * 3。 首先匹配的是 expression,由于目前 expression 往下分只有一种可能,即 addExpression,所以分解为 addExpression。依次类推,接下来的顺序为 mulExpression、term、1(integerConstant)、+(op)、mulExpression、term、2(integerConstant)、*(op)、mulExpression、term、3(integerConstant)。 如下图所示: 这里可能会有人有疑问,为什么一个表达式搞得这么复杂,expression 下面有 addExpression,addExpression 下面还有 mulExpression。其实这里是为了考虑运算符优先级而设的,mulExpr 比 addExpr 表达式运算级要高。 1 + 2 * 3compileExpression | compileAddExpr | | compileMultExpr | | | compileTerm | | | |_ matches integerConstant push 1 | | |_ | | matches '+' | | compileMultExpr | | | compileTerm | | | |_ matches integerConstant push 2 | | | matches '*' | | | compileTerm | | | |_ matches integerConstant push 3 | | |_ compileOp('*') * | |_ compileOp('+') + |_有很多算法可用来构建语法分析树,这里只讲两种算法。 ...

June 30, 2019 · 2 min · jiezi

编译原理实战入门用-JavaScript-写一个简单的四则运算编译器一词法分析

编译器编译器是一个程序,作用是将一门语言翻译成另一门语言。 一般的程序,CPU 是无法直接执行的,因为 CPU 只能识别机器指令。所以要想执行一个程序,首先要将高级语言编写的程序翻译为汇编代码,再将汇编代码翻译为机器指令,这样 CPU 才能识别并执行。 示例: // CPU 无法识别10 + 5// 翻译成汇编语言push 10push 5add// 最后翻译为机器指令 汇编代码和机器指令一一对应// 机器指令由 1 和 0 组成,以下指令非真实指令,只做演示用001110100101010111010100111001010010100111100001学会编译原理有什么好处? 对编译过程内部原理的掌握将会使你成为更好的高级程序员。 词法分析程序其实就是保存在文本文件中的一系列字符,词法分析的作用是将这一系列字符按照某种规则分解成一个个字元(token,也称为终结符),忽略空格和注释。 示例: // 程序代码10 + 5 + 6// 词法分析后得到的 token10+5+6终结符终结符就是语言中用到的基本元素,一般不能再被分解。 四则运算中的终结符包括符号和整数常量(暂不支持一元操作符)。 符号:+ - * / ( ) 整数常量:12、1000、111... 词法分析代码实现function lexicalAnalysis(expression) { const symbol = ['(', ')', '+', '-', '*', '/'] const re = /\d/ const tokens = [] const chars = expression.trim().split('') let token = '' chars.forEach(c => { if (re.test(c)) { token += c } else if (c == ' ' && token) { tokens.push(token) token = '' } else if (symbol.includes(c)) { if (token) { tokens.push(token) token = '' } tokens.push(c) } }) if (token) { tokens.push(token) } return tokens}console.log(lexicalAnalysis('100 + 23 + 34 * 10 / 2')) // ["100", "+", "23", "+", "34", "*", "10", "/", "2"]编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(一)词法分析编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(二)语法分析编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(三)模拟执行编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(四)结语完整源码参考资料:计算机系统要素

June 30, 2019 · 1 min · jiezi

编译原理课设-词法分析

编译原理课设 第一阶段GUET编译原理课设 词法分析主要参考了 GUET_曼陀罗华 的博客(好像是位研究生姐姐),改了其中一些部分,在小姐姐基础上增加了出错控制。其实有更好的方式,不过前期我是这样写的,就先贴出来,循序渐进。正文如下:词法分析是干什么的: 过滤掉所有的空格、换行、注释把有用的东西存到 pascal[ ] 数组中比如 BEGIN 存到pascal[0] 比如 VAR 存到pascal[1] 中...这个数组可以用于以后的语法和语义分析 pascal[ ] 数组是dual 类型的,除了保存单词的信息,还有单词的种类dual_type这里写出思路,具体的小细节根据实际情况修改。比如 关键字数组 、 类型码 等 符号编号助记符结束符0FINISHBEGIN1BEGINEND2ENDIF3IFTHEN4THENWHILE5WHILEELSE6ELSEDO7DOVAR8VARINTEGER9INTEGER整数10INT标识符11ID+101ADD-102SUB*103MUL/104DIV>105GT=106EQ<107LT:108COLON:=109COL_EQ<>110NE<=111LE>=112GE;113FIN//114 /*115 */116 #include<stdio.h>#include<stdlib.h>#include<string.h>#include<iostream>#include<math.h>#include<ctype.h>#include<iomanip>using namespace std;struct dual { int dual_type; union { char lexeme_text[50]; int lexeme_num[50]; }lexeme; int x; int y;} DUAL[100];//校验通过的单词,存入到pascal中int pasnum = 0;dual pascal[100];//当前数组 DUAL 下标int num = 0;//关键字数组const char * keyword[] = { "sign","BEGIN","END","IF","THEN","WHILE" ,"ELSE","DO","VAR","INTEGER","INT" };//单分界符char singword[10] = "+-*=(),;";//双分界符打头,注释包含在这里char doubleword[10] = "><:/";//类型和帮记符int type[31] = {0,1,2,3,4,5,6,7,8,9,10,11,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119};const char * typesign[31] = { "FINISH","BEGIN","END","IF","THEN","WHILE" ,"ELSE","DO","VAR","INTEGER","INT","ID","ADD","SUB","MUL","DIV","GT","EQ","LT","COLON","COL_EQ","NE","LE","GE","FIN","ANND","ANNF","ANNL","CO","LL","RR" };int findSignIndex(int dual_type) { int i = 0; for (i; i < 31; i++){ if (type[i] == dual_type) { return i; } } return 0;}//整型数组转整数int toint(int lexeme_text[]) { int i = 0, length = 0, sum = 0; for (length; lexeme_text[length] != -1; length++); for (i; i < length; i++) { sum += lexeme_text[i] * pow(10, length - i - 1); } return sum;}//是否单分界符元素int isSingle(char ch) { int i; for (i = 0; i < 10; i++) { if (ch == singword[i]) { return 1; } } return 0;}//是否双分界符开头int isDoubelStar(char ch) { int i; for (i = 0; i < 5; i++) { if (ch == doubleword[i]) { return 1; } } return 0;}//处理单分界符元素//设置标识符类型//结构体,类型,字符值int handSingle(dual dual_element, int dual_type, char ch) { dual_element.dual_type = dual_type; dual_element.lexeme.lexeme_text[0] = ch; dual_element.lexeme.lexeme_text[0] = '\0'; //cout << "匹配到" << ch << endl; return 1;}//出错消息控制int errMsg(int err_type, int row, int column, const char msg[]) { cout << "Error" << err_type << ":" << row << "行 " << column << "列" << " 原因:" << msg << endl; return 1;}int scaner() { char ch; int i, j; int row = 1; int clumn = 1; int scan_success_flag = 1; FILE * file; file = fopen("a.txt", "r"); if (file == NULL) { return 0; } //通过getc获取字符 ch = getc(file); while (ch != EOF) { //换行 while (ch == '\n') { row++; clumn = 1; ch = getc(file); } //空格和tab,定义他们的长度都为1 while (ch == ' ' || ch == '\t') { clumn++; ch = getc(file); } //是字母 if (isalpha(ch)) { DUAL[num].lexeme.lexeme_text[0] = ch; //review DUAL[num].x = clumn; DUAL[num].y = row; //Token 下标移动到1 j = 1; ch = getc(file); clumn++; //抽取出来,做成检验函数,排除其他可能 while (isalpha(ch)) { DUAL[num].lexeme.lexeme_text[j++] = ch; ch = getc(file); clumn++; //j > 8说明单词超长,为了防止多次输出错误信息,设置为9 if (j == 9) { //cout << "单词超长" <<endl; errMsg(1011, row, clumn - 9, "单词超长"); scan_success_flag = 0; } } DUAL[num].lexeme.lexeme_text[j] = '\0'; //单词扫描结束,查关键字,抽取函数 for (i = 0; i < 11; i++) { if (strcmp(DUAL[num].lexeme.lexeme_text, keyword[i]) == 0) { DUAL[num].dual_type = i; pascal[pasnum++] = DUAL[num]; //printf("匹配到%s\n",keyword[i]); break; } } //标识符 if (i == 11) { DUAL[num].dual_type = 11; pascal[pasnum++] = DUAL[num]; } num++; } //是数字 else if (isdigit(ch)) { DUAL[num].lexeme.lexeme_num[0] = ch - '0'; DUAL[num].lexeme.lexeme_num[1] = -1; DUAL[num].x = row; DUAL[num].y = clumn; ch = getc(file); j = 1; while (isdigit(ch)) { DUAL[num].lexeme.lexeme_num[j++] = ch - '0'; DUAL[num].lexeme.lexeme_num[j] = -1; clumn++; //如果上一个ch是非数字后缀,错误??? 忘了 if (!isdigit(ch)) { DUAL[num].dual_type = -1; errMsg(1012, row, clumn, "非数字后缀"); scan_success_flag = 0; } ch = getc(file); } if (DUAL[num].dual_type == -1) { } //判断是否溢出,这里的转换需要review,初始化都为0的情况需要考虑 else { //int tempnum = toint(DUAL[num].lexeme.lexeme_num,j); int tempnum = toint(DUAL[num].lexeme.lexeme_num); if (tempnum > 65535 || tempnum < 0) { errMsg(1013, row, clumn, "数字过大,溢出"); scan_success_flag = 0; } else { DUAL[num].dual_type = 10;//整数类型 pascal[pasnum] = DUAL[num]; pasnum++; //cout << "匹配到数字" << tempnum << endl; } } num++; } //只可能是单分界符开头的,提取首字符位置,类似 indexOf 函数 else if (isSingle(ch)) { DUAL[num].x = row; DUAL[num].y = clumn++; DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = '\0'; //cout << "匹配到" << ch << endl; switch (ch) { case '+': DUAL[num].dual_type = 101; break; case '-': DUAL[num].dual_type = 102; break; case '*': DUAL[num].dual_type = 103; break; //todo 除属于双分界符情况,不在此讨论 case '=': DUAL[num].dual_type = 106; break; case ';': DUAL[num].dual_type = 113; break; case ',': DUAL[num].dual_type = 117; break; case '(': DUAL[num].dual_type = 118; break; case ')': DUAL[num].dual_type = 119; break; default: cout << "isSingle出错:" << ch << endl; break; } pascal[pasnum] = DUAL[num]; pasnum++; ch = getc(file); num++; } //双分界开头的 else if (isDoubelStar(ch)) { int isNote = 0; //默认不是注释 //DUAL[num].lexeme.lexeme_text[0] = ch; 因为注释就不能存放进去 //DUAL[num].lexeme.lexeme_text[1] = '\0'; char next_ch = getc(file); switch (ch) { case '<': //如果下一个是=,那么就是<= if (next_ch == '=') { DUAL[num].dual_type = 111; DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = next_ch; DUAL[num].lexeme.lexeme_text[2] = '\0'; ch = getc(file); //cout << "<=" << endl; } else if (next_ch == '>') { DUAL[num].dual_type = 110; DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = next_ch; DUAL[num].lexeme.lexeme_text[2] = '\0'; ch = getc(file); //cout << "< >" << endl; } else { //否则是单分界 DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = '\0'; DUAL[num].dual_type = 107; //作用相当于 getc(file),为下一次进入一级while循环做准备 ch = next_ch; //cout << "<" << endl; } break; case '>': //如果下一个是=,那么就是>= if (next_ch == '=') { DUAL[num].dual_type = 112; DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = next_ch; DUAL[num].lexeme.lexeme_text[2] = '\0'; ch = getc(file); //cout << ">=" << endl; } else { //否则是单分界 DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = '\0'; DUAL[num].dual_type = 105; ch = next_ch; //cout << ">" << endl; } break; case ':': //如果下一个是=,那么就是:= if (next_ch == '=') { DUAL[num].dual_type = 109; DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = next_ch; DUAL[num].lexeme.lexeme_text[2] = '\0'; ch = getc(file); //cout << ":=" << endl; } else { //否则出错 DUAL[num].dual_type = 108; ch = next_ch; errMsg(1014, row, clumn, "期待的 '=' 没有出现,':'之后缺少 '=' "); scan_success_flag = 0; } break; case '/': //单行注释 if (next_ch == '/') { row++; clumn = 1; isNote = 1; //cout << "// 检测到单行注释" << endl; ch = getc(file); while (ch != '\n') { ch = getc(file); } } //多行注释 else if (next_ch == '*') { isNote = 1; char ch1 = getc(file); char ch2 = getc(file); while (ch1 != '*' || ch2 != '/') { //处理坐标 if (ch1 == '\n') { row++; clumn = 1; } else { clumn++; } if (ch2 == '\n') { row++; clumn = 1; } else { clumn++; } //分析字符 if (ch2 == '*') { ch1 = ch2; ch2 = getc(file); } //包含了ch1 == ‘*’且ch2 != '/的情况 else { ch1 = getc(file); ch2 = getc(file); } //出错控制 if (ch1 == EOF || ch2 == EOF) { //没有期待的/出现或者已经到头 //cout << "多行注释出错" << endl; errMsg(1015, row, clumn, "没有期待的 '*/' 出现,不合法的注释"); break; } } ch = getc(file); } //排除其他可能,这只是一个单纯除号 else { DUAL[num].dual_type = 104; DUAL[num].lexeme.lexeme_text[0] = ch; DUAL[num].lexeme.lexeme_text[1] = '\0'; //作用相当于 getc(file),为下一次进入一级while循环做准备 ch = next_ch; } default: break; } if (!isNote) { pascal[pasnum] = DUAL[num]; pasnum++; num++; } } //其他字符 else { errMsg(1016, row, clumn, "非法字符"); scan_success_flag = 0; ch = getc(file); } } return scan_success_flag;}int main() { int i; if (scaner()) { cout << "====== 分析成功 ======" << endl; } cout << endl << "====== 输出扫描合法的词元记录 ======" << endl; cout << endl << " 单词 类型 助记符" << endl; for (i = 0; i < pasnum; i++) { //整数类型 if (pascal[i].dual_type == 10) { cout << " " << std::left << setw(12) << toint(pascal[i].lexeme.lexeme_num) << setw(8) << pascal[i].dual_type << typesign[findSignIndex(pascal[i].dual_type)]<< endl; } else { cout << " " << std::left << setw(12) << pascal[i].lexeme.lexeme_text << setw(8) << pascal[i].dual_type << typesign[findSignIndex(pascal[i].dual_type)] << endl; findSignIndex(pascal[i].dual_type); } } system("pause"); return 0;} ...

June 25, 2019 · 6 min · jiezi

编译原理课设-SLR语法分析

语法分析GUET编译原理课设,当时找了很多资料,网上的资料乱七八糟,花了哥哥我好久时间。致谢:代码的算法有参考书本其他博主的,表示感谢,同时也更正他们的了一些微小错误和不合理地方。这里,我用书本的例子数据作为测试。 西北工业大学出版社《编译原理》蒋立源 第3版第157页代码中的约定: 用 0 表示空用102表示移进 s2用 53 表示规约 r3#include <iostream>#include<stdio.h>#include<stdlib.h>using namespace std;//0为空白//102表示S2//51表示r1int actionTable[12][6] = { 102,0,0,0,0,0, 0,0,0,0,0,-1, 0,104,0,0,0,0, 0,0,105,0,0,0, 0,0,53,0,0,0, 0,107,0,108,0,0, 0,0,0,0,109,0, 0,0,52,0,0,0, 0,0,110,0,55,0, 0,0,0,0,0,51, 0,0,0,108,0,0, 0,0,0,0,54,0};int gotoTable[12][3] = { 1,0,0,0,0,0, 0,3,0,0,0,0, 0,0,0,0,0,6, 0,0,0,0,0,0, 0,0,0,0,0,0, 0,0,11,0,0,0};//语法表,存储读取的文法char Grammar[20][10] = { '\0' };char vtArray[10], vnArray[10];//action表横坐标数组:用于定位,查表char actionAxis[6] = { 'b', 'd', 't', 's', 'e', '#' };//goto表横坐标数组:用于定位,查表char gotoAxis[3] = { 'B', 'D', 'S' };//非终结符数量,终结符数量,状态数量int vnNumber, vtNumber, stateNumber = 12;int LR[10];int grammarNum;typedef struct { char * base; char * top;}SymbolStack;typedef struct { int * base; int * top;}StateStack;StateStack state;SymbolStack symbol;//扫描文法文件,得到文法//左部符号存储在第一列,即 Grammar[i][0]int scanGrammar() { FILE * fp = fopen("slr.txt", "r"); FILE * tp; char ch, nextCh; int i = 0, j = 0, k, count; while (!feof(fp)) { fscanf(fp, "%c", &ch); //文件结束标志 if (ch == '?') { Grammar[i][j] = '\0'; break; } if (ch == '\n') { Grammar[i][j] = '\0'; //转移到下一行第一列 i++; j = 0; continue; } if (ch == '-') { tp = fp; fscanf(tp, "%c", &nextCh); if (nextCh == '>') { fp = tp; continue; } } if (ch == '|') { Grammar[i + 1][0] = Grammar[i][0]; Grammar[i][j] = '\0'; i++; j = 1; continue; } Grammar[i][j] = ch; if (ch >= 'A' && ch <= 'Z') { count = 0; while (vnArray[count] != ch && vnArray[count] != '\0') { count++; } if (vnArray[count] == '\0') { vnNumber = count + 1; //拓广文法的开始符W if (ch == 'W') { j++; continue; } vnArray[count] = ch; vnNumber = count + 1; } } else { count = 0; while (vtArray[count] != ch && vtArray[count] != '\0') { count++; } if (vtArray[count] == '\0') { vtArray[count] = ch; vtNumber = count + 1; } } j++; } cout << "输入的文法:" << endl; for (k = 0; k <= i; k++) { j = 0; while (Grammar[k][j] != '\0') { if (j == 1) { printf("->"); } printf("%c", Grammar[k][j]); j++; } cout << endl; } count = 0; cout << endl << "vn:" << endl; while (vtArray[count] != '\0') { printf("%3c", vtArray[count]); count++; } vtArray[count] = '#'; vtNumber = count + 1; printf("%3c", vtArray[count]); cout << endl << "vn:" << endl; count = 0; while (vnArray[count] != '\0') { printf("%3c", vnArray[count]); count++; } cout << endl; fclose(fp); grammarNum = i + 1; return i;}//计算LR(0)项目长度,决定了:规约时符号栈和状态栈的出栈次数int getLRLength() { int i, j; for (i = 0; i < grammarNum; i++) { j = 1; while (Grammar[i][j] != '\0') { j++; } //产生式 i 的长度为 j LR[i] = j; } return 0;}//创建栈void createStack() { state.base = (int *)malloc(100 * sizeof(int)); if (!state.base) exit(1); state.top = state.base; *state.top = 0; symbol.base = (char *)malloc(100 * sizeof(char)); if (!symbol.base) exit(1); symbol.top = symbol.base; *symbol.top = '#';}//查 Action 表int getActionValue(int stateTop, char ch) { int i, j; //查找 //更改 i 为 stateTop /*for (i = 0; i < stateNumber; i++) { if (stateTop == i) { break; } }*/ //查找横坐标数组actionAxis,定位 for (j = 0; j < vtNumber; j++) { if (ch == actionAxis[j]) break; } return actionTable[stateTop][j];}//查 Goto 表int getGotoValue(int stateTop, char inputChar) { int i, j; for (j = 0; j < vnNumber; j++) { if (inputChar == gotoAxis[j]) break; } if (gotoTable[stateTop][j] == 0) { //cout << "Error" << endl; } return gotoTable[stateTop][j];}//输出信息int printInfo(int count, int i, char chars[], int actionValue) { int * p = state.base, stateNum; int j, jj; char * q = symbol.base, symbolNum; printf("%d\t", count); while (p != state.top + 1) { stateNum = *p; printf("%d", stateNum); p++; } printf("\t"); while (q != symbol.top + 1) { symbolNum = *q; printf("%c", symbolNum); q++; } printf("\t"); j = i; jj = 0; while (jj < j) { printf(" "); jj++; } while (chars[j] != '\0') { printf("%c", chars[j]); j++; } if (actionValue > 100) { cout << "\t\ts" << actionValue - 100 << "\t" << actionValue - 100 << endl; } //如果是规约,把预留下一状态给规约程序输出,在此不清除下一状态为多少 else if (actionValue > 50) { cout << "\t\tr" << actionValue - 50 << "\t"; } else if (actionValue == -1) { cout << "\t\tacc" << "\n\n可以识别的输入串\n" << endl; } else { return 0; } return 1;}//规约 -- 根据编号为 i 的产生式进行规约//规约的结果是一个非终结符号,即 Grammar[i][0]int redution(int i) { int * p, stateNum, gotoValue, LRLength; LRLength = LR[i] - 1; while (LRLength != 0) { state.top--; symbol.top--; LRLength--; } int temp = *state.top; state.top++; *state.top = getGotoValue(temp, Grammar[i][0]); symbol.top++; *symbol.top = Grammar[i][0]; stateNum = *state.top; if (*state.top == 0) { return *state.top; } return *state.top;}//扫描输入字符串int scanChars() { char chars[20] = { 'b', 'd', 't', 's', 'e','#' }; int i = 0, stepCount = 1; int actionValue, nextActionValue; int stateTop, gotoValue; //scanf("%s", &chars); cout << "步骤\t状态\t栈中\t余留符号 分析动作 下一状态" << endl; while (chars[i] != '\0') { stateTop = *state.top; //向前查看一个符号,查action表 actionValue = getActionValue(stateTop, chars[i]); //如果是移进,就可以输出下一状态 //如果是规约,则等到规约完毕再输出下一状态,因为现在不可知 printInfo(stepCount, i, chars, actionValue); //数组(表格)的值 if (actionValue == 0) { cout <<endl<< "不合法的字符串!" << endl; break; } //acc 接受 if (actionValue == -1) { stepCount++; return 1; } //s 移进 if (actionValue >= 100) { actionValue = actionValue - 100; state.top++; *state.top = actionValue; symbol.top++; *symbol.top = chars[i]; i++; stepCount++; } //r 规约 if (actionValue >= 50 && actionValue < 100) { actionValue = actionValue - 50; gotoValue = redution(actionValue); //规约完毕后,输出下一状态 cout << gotoValue << endl; stepCount++; } } return 0;}int main() { scanGrammar(); getLRLength(); createStack(); scanChars(); system("pause"); return 0;}文法 slr.txt问号结尾 ...

June 25, 2019 · 5 min · jiezi

编译原理学习一去除代码中的注释

前言开始学习编译原理了耶~关于编译原理的所有练习,按照老规矩,还是用我最喜欢的C#语言来实现,运行在.NetCore平台上~关于这个系列的所有代码已经上传到github了,项目主页: https://github.com/Deali-Axy/CompilerConstructionLearning本次题目对C或C++等高级程序设计语言编写的源程序中的//注释和/…/注释进行删除,保留删除后的源程序。要求以文件形式进行保存。思路分析程序主要功能就是消除已经编写好的源程序中的注释。在源程序中注释有两种形式,一种是单行注释,用“//”表示,另一种是多行注释,用“/…/”表示。针对这两种形式,程序中用了if..else..语句加以判断,并做出相应的处理。在这里还有可能出现另一种情况,上述两种注释符号可能出现在引号中,出现在引号中的注释符号并没有注释功能,因此在引号中出现的注释符号不应该被消除。所以,这次编写的程序将要分三种情况分析。第一种情况,单行注释:if (ch != temp){ // 这里就是单行注释 ofile.put(ch); ch = ifile.get();}或者 if (ch != temp){ /* 这里就是单行注释 */ ofile.put(ch); ch = ifile.get();}第二种情况,块注释:if (ifile.fail() || ofile.fail()){ cerr << "open file fail\n"; return EXIT_FAILURE; /*返回值EXIT_FAILURE(在cstdlib库中定义),用于向操作系统报* 告打开文件失败*/}第三种情况,行后注释:ifile.close(); // 关闭文件ofile.close();cout << "/////*////ret/rtr////";system("pause");return 0;还有一个关键的注意点可以看到这一行 cout << "/////*////ret/rtr////";这个字符串用双引号包起来的代码中有很多斜杠,所以要避免将这些斜杠识别为注释。这里我用的方法是在处理注释前先把包含注释符号的字符串替换掉,等注释删除之后,再添加回去。 实现代码注释写得很详细啦,配合上面的思路分析,我就不再继续分析代码了~ var sReader = new StreamReader(filePath);var newSource = "";var inBlock = false;var replaceFlag = false;var tempLine = ""; // 用于保存被替换的特殊行代码while (!sReader.EndOfStream){ var line = sReader.ReadLine(); if (line.Length == 0) continue; // 去除空行 var quotationPattern = "^(.*?)\".*//.*\""; var quotationResult = Regex.Match(line, quotationPattern); if (quotationResult.Success) { System.Console.WriteLine("替换特殊代码,双引号中包裹注释斜杠"); tempLine = quotationResult.Groups[0].Value; replaceFlag = true; line = Regex.Replace(line, quotationPattern, REPLACEMENT); } // 单行注释 if (line.Trim().StartsWith(@"//")) continue; if (line.Trim().StartsWith(@"/*") && line.EndsWith(@"*/")) continue; // 注释块 if (Regex.Match(line.Trim(), @"^/\*").Success) inBlock = true; if (Regex.Match(line.Trim(), @"\*/$").Success) { inBlock = false; continue; } // 行后注释 // 使用非贪婪模式(.+?)匹配第一个// var pattern = @"^(.*?)//(.*)"; // var pattern = @"[^(.*?)//(.*)]|[^(.*?)/\*(.*)\*/]"; var result = Regex.Match(line, pattern); if (result.Success) { System.Console.WriteLine("发现行后注释:{0}", result.Groups[2]); line = result.Groups[1].Value; } // 还原被替换的代码 if (replaceFlag) { System.Console.WriteLine("还原特殊代码"); line = line.Replace(REPLACEMENT, tempLine); replaceFlag = false; } if (inBlock) continue; newSource += line + Environment.NewLine;}var outputPath = "output/exp1.src";System.Console.WriteLine("去除注释完成,创建新文件。");using (var sWriter = new StreamWriter(outputPath)){ sWriter.Write(newSource);}System.Console.WriteLine("操作完成!文件路径:{0}", outputPath);结果测试源文件#include <iostream>#include <fstream>#include <iomanip>#include <cstdlib>using namespace std;int main(){ cout << '/'; ifstream ifile; //建立文件流对象 ofstream ofile; ifile.open("f:\\上机实验题\\C++\\ConsoleApplication2\\ConsoleApplication2\\源.cpp"); //打开F盘根目录下的fileIn.txt文件 ofile.open("f:\\上机实验题\\C++\\ConsoleApplication2\\ConsoleApplication2\\源.obj"); if (ifile.fail() || ofile.fail()) { //测试打开操作是否成功 cerr << "open file fail\n"; return EXIT_FAILURE; /*返回值EXIT_FAILURE(在cstdlib库中定义),用于向操作系统报* 告打开文件失败*/ } char ch; ch = ifile.get(); //进行读写操作 while (!ifile.eof()) { if (ch == 34) { //双引号中若出现“//”,双引号中的字符不消除 char temp = ch; //第一个双引号 ofile.put(ch); ch = ifile.get(); while (!ifile.eof()) { if (ch != temp) { //寻找下一个双引号 ofile.put(ch); ch = ifile.get(); } else { ofile.put(ch); break; } } ch = ifile.get(); continue; //双引号情况结束,重新新一轮判断 } if (ch == 47) { //出现第一个斜杠 char temp2 = ch; ch = ifile.get(); if (ch == 47) { //单行注释情况 ch = ifile.get(); while (!(ch == '\n')) ch = ifile.get(); } else if (ch == '*') { //多行注释情况 while (1) { ch = ifile.get(); while (!(ch == '*')) ch = ifile.get(); ch = ifile.get(); if (ch == 47) break; } ch = ifile.get(); } else { ofile.put(temp2); //temp2保存第一个斜杠,当上述两种情况都没有时,将此斜杠输出 } //ch = ifile.get(); } //cout << ch << endl; ofile.put(ch); //将字符写入文件流对象中 ch = ifile.get(); //从输入文件对象流中读取一个字符 } ifile.close(); //关闭文件 ofile.close(); cout << "/////*////ret/rtr////"; system("pause"); return 0;}处理后的结果#include <iostream>#include <fstream>#include <iomanip>#include <cstdlib>using namespace std;int main(){ cout << '/'; ifstream ifile; ofstream ofile; ifile.open("f:\\上机实验题\\C++\\ConsoleApplication2\\ConsoleApplication2\\源.cpp"); ofile.open("f:\\上机实验题\\C++\\ConsoleApplication2\\ConsoleApplication2\\源.obj"); if (ifile.fail() || ofile.fail()) { cerr << "open file fail\n"; return EXIT_FAILURE; } char ch; ch = ifile.get(); while (!ifile.eof()) { if (ch == 34) { char temp = ch; ofile.put(ch); ch = ifile.get(); while (!ifile.eof()) { if (ch != temp) { ofile.put(ch); ch = ifile.get(); } else { ofile.put(ch); break; } } ch = ifile.get(); continue; } if (ch == 47) { char temp2 = ch; ch = ifile.get(); if (ch == 47) { ch = ifile.get(); while (!(ch == '\n')) ch = ifile.get(); } else if (ch == '*') { while (1) { ch = ifile.get(); while (!(ch == '*')) ch = ifile.get(); ch = ifile.get(); if (ch == 47) break; } ch = ifile.get(); } else { ofile.put(temp2); } } ofile.put(ch); ch = ifile.get(); } ifile.close(); ofile.close(); cout << "/////*////ret/rtr////"; system("pause"); return 0;}完整代码https://github.com/Deali-Axy/CompilerConstructionLearning/blob/master/code/exp/exp1/Exp1.cs ...

May 11, 2019 · 3 min · jiezi

编译原理学习

编译过程包括:预处理 - 词法分析 - 语法分析 - 生成中间代码 - 生成目标代码 - 汇编 - 链接

May 5, 2019 · 1 min · jiezi

编译原理 一 绪论

程序设计语言和编译程序用助记符 代替机器语言(二进制编码)的另一种语言,汇编语言。汇编语言编写的程序必须翻译成机器语言才能执行,这种翻译是通过“汇编程序”实现的。编译程序就是指这样一种程序,通过它能够将用高级语言编写的源程序转换为与之在逻辑上等价的低级语言形式的目标程序。一个高级语言程序的执行分为两个阶段:编译阶段。将源程序变换成目标程序。运行阶段。接收输入数据,运行后输出运算结果。如果编译生成的目标程序是汇编语言形式的,那么在编译与运行阶段之间还有一个汇编阶段。高级语言程序也有通过解释程序来执行的。特点是:逐条读出语句并解释执行。(边解释边执行)解释程序的执行过程中并不产生目标程序。典型的例子是 BASIC 语言。编译过程分为五个阶段:词法分析:将源程序中的字符串变换为单词符号流的过程。语法分析:将单词符号流分解为各类语法单位(语法范畴)。语义分析和中间代码生成:对各类语法范畴进行静态语义检查。如变量是否定义,类型是否正确。检查正确的话进行中间代码的翻译优化任务是对中间代码进行等价变换或改造,获得更为高效的目标代码。优化遵守程序的等价变换原则目标代码生成把优化后的中间代码变换为机器语言程序或者汇编语言程序。注意点:一个编译过程分多遍完成可以使编译程序的逻辑结构更清晰编译过程的绝大部分时间都花在表格管理上为了尽可能多的发现错误,应该在发现错误后还能继续编译下去开发一个编译程序常采用下列技术实现:自编译用某种高级语言编写自己的编译程序称为自编译。交叉编译指用 a 机器上的编译程序来产生可在 b 机器上运行的目标代码。自展移植

April 11, 2019 · 1 min · jiezi

精读《syntax-parser 源码》

引言syntax-parser 是一个 JS 版语法解析器生成器,具有分词、语法树解析的能力。通过两个例子介绍它的功能。第一个例子是创建一个词法解析器 myLexer:import { createLexer } from “syntax-parser”;const myLexer = createLexer([ { type: “whitespace”, regexes: [/^(\s+)/], ignore: true }, { type: “word”, regexes: [/^([a-zA-Z0-9]+)/] }, { type: “operator”, regexes: [/^(+)/] }]);如上,通过正则分别匹配了 “空格”、“字母或数字”、“加号”,并将匹配到的空格忽略(不输出)。分词匹配是从左到右的,优先匹配数组的第一项,依此类推。接下来使用 myLexer:const tokens = myLexer(“a + b”);// tokens:// [// { “type”: “word”, “value”: “a”, “position”: [0, 1] },// { “type”: “operator”, “value”: “+”, “position”: [2, 3] },// { “type”: “word”, “value”: “b”, “position”: [4, 5] },// ]‘a + b’ 会按照上面定义的 “三种类型” 被分割为数组,数组的每一项都包含了原始值以及其位置。第二个例子是创建一个语法解析器 myParser:import { createParser, chain, matchTokenType, many } from “syntax-parser”;const root = () => chain(addExpr)(ast => ast[0]);const addExpr = () => chain(matchTokenType(“word”), many(addPlus))(ast => ({ left: ast[0].value, operator: ast[1] && ast[1][0].operator, right: ast[1] && ast[1][0].term }));const addPlus = () => chain("+"), root)(ast => ({ operator: ast[0].value, term: ast[1] }));const myParser = createParser( root, // Root grammar. myLexer // Created in lexer example.);利用 chain 函数书写文法表达式:通过字面量的匹配(比如 + 号),以及 matchTokenType 来模糊匹配我们上面词法解析出的 “三种类型”,就形成了完整的文法表达式。syntax-parser 还提供了其他几个有用的函数,比如 many optional 分别表示匹配多次和匹配零或一次。接下来使用 myParser:const ast = myParser(“a + b”);// ast:// [{// “left”: “a”,// “operator”: “+”,// “right”: {// “left”: “b”,// “operator”: null,// “right”: null// }// }]2. 精读按照下面的思路大纲进行源码解读:词法解析词汇与概念分词器语法解析词汇与概念重新做一套 “JS 执行引擎”实现 Chain 函数引擎执行何时算执行完“或” 逻辑的实现many, optional, plus 的实现错误提示 & 输入推荐First 集优化词法解析词法解析有点像 NLP 中分词,但比分词简单的时,词法解析的分词逻辑是明确的,一般用正则片段表达。词汇与概念Lexer:词法解析器。Token:分词后的词素,包括 value:值、position:位置、type:类型。分词器分词器 createLexer 函数接收的是一个正则数组,因此思路是遍历数组,一段一段匹配字符串。我们需要这几个函数:class Tokenizer { public tokenize(input: string) { // 调用 getNextToken 对输入字符串 input 进行正则匹配,匹配完后 substring 裁剪掉刚才匹配的部分,再重新匹配直到字符串裁剪完 } private getNextToken(input: string) { // 调用 getTokenOnFirstMatch 对输入字符串 input 进行遍历正则匹配,一旦有匹配到的结果立即返回 } private getTokenOnFirstMatch({ input, type, regex }: { input: string; type: string; regex: RegExp; }) { // 对输入字符串 input 进行正则 regex 的匹配,并返回 Token 对象的基本结构 }}tokenize 是入口函数,循环调用 getNextToken 匹配 Token 并裁剪字符串直到字符串被裁完。语法解析语法解析是基于词法解析的,输入是 Tokens,根据文法规则依次匹配 Token,当 Token 匹配完且完全符合文法规范后,语法树就出来了。词法解析器生成器就是 “生成词法解析器的工具”,只要输入规定的文法描述,内部引擎会自动做掉其余的事。这个生成器的难点在于,匹配 “或” 逻辑失败时,调用栈需要恢复到失败前的位置,而 JS 引擎中调用栈不受代码控制,因此代码需要在模拟引擎中执行。词汇与概念Parser:语法解析器。ChainNode:连续匹配,执行链四节点之一。TreeNode:匹配其一,执行链四节点之一。FunctionNode:函数节点,执行链四节点之一。MatchNode:匹配字面量或某一类型的 Token,执行链四节点之一。每一次正确的 Match 匹配都会消耗一个 Token。重新做一套 “JS 执行引擎”为什么要重新做一套 JS 执行引擎?看下面的代码:const main = () => chain(functionA(), tree(functionB1(), functionB2()), functionC());const functionA = () => chain(“a”);const functionB1 = () => chain(“b”, “x”);const functionB2 = () => chain(“b”, “y”);const functionC = () => chain(“c”);假设 chain(‘a’) 可以匹配 Token a,而 chain(functionC)) 可以匹配到 Token c。当输入为 a b y c 时,我们该怎么写 tree 函数呢?我们期望匹配到 functionB1 时失败,再尝试 functionB2,直到有一个成功为止。那么 tree 函数可能是这样的:function tree(…funs) { // … 存储当前 tokens for (const fun of funs) { // … 复位当前 tokens const result = fun(); if (result === true) { return result; } }}不断尝试 tree 中内容,直到能正确匹配结果后返回这个结果。由于正确的匹配会消耗 Token,因此需要在执行前后存储当前 Tokens 内容,在执行失败时恢复 Token 并尝试新的执行链路。这样看去很容易,不是吗?然而,下面这个例子会打破这个美好的假设,让我们稍稍换几个值吧:const main = () => chain(functionA(), tree(functionB1(), functionB2()), functionC());const functionA = () => chain(“a”);const functionB1 = () => chain(“b”, “y”);const functionB2 = () => chain(“b”);const functionC = () => chain(“y”, “c”);输入仍然是 a b y c,看看会发生什么?线路 functionA -> functionB1 是 a b y 很显然匹配会通过,但连上 functionC 后结果就是 a b y y c,显然不符合输入。此时正确的线路应该是 functionA -> functionB2 -> functionC,结果才是 a b y c!我们看 functionA -> functionB1 -> functionC 链路,当执行到 functionC 时才发现匹配错了,此时想要回到 functionB2 门也没有!因为 tree(functionB1(), functionB2()) 的执行堆栈已退出,再也找不回来了。所以需要模拟一个执行引擎,在遇到分叉路口时,将 functionB2 保存下来,随时可以回到这个节点重新执行。实现 Chain 函数用链表设计 Chain 函数是最佳的选择,我们要模拟 JS 调用栈了。const main = () => chain(functionA, [functionB1, functionB2], functionC)();const functionA = () => chain(“a”)();const functionB1 = () => chain(“b”, “y”)();const functionB2 = () => chain(“b”)();const functionC = () => chain(“y”, “c”)();上面的例子只改动了一小点,那就是函数不会立即执行。chain 将函数转化为 FunctionNode,将字面量 a 或 b 转化为 MatchNode,将 [] 转化为 TreeNode,将自己转化为 ChainNode。我们就得到了如下的链表:ChainNode(main) └── FunctionNode(functionA) ─ TreeNode ─ FunctionNode(functionC) │── FunctionNode(functionB1) └── FunctionNode(functionB2)至于为什么 FunctionNode 不直接展开成 MatchNode,请思考这样的描述:const list = () => chain(’,’, list)。直接展开则陷入递归死循环,实际上 Tokens 数量总有限,用到再展开总能匹配尽 Token,而不会无限展开下去。那么需要一个函数,将 chain 函数接收的不同参数转化为对应 Node 节点:const createNodeByElement = ( element: IElement, parentNode: ParentNode, parentIndex: number, parser: Parser): Node => { if (element instanceof Array) { // … return TreeNode } else if (typeof element === “string”) { // … return MatchNode } else if (typeof element === “boolean”) { // … true 表示一定匹配成功,false 表示一定匹配失败,均不消耗 Token } else if (typeof element === “function”) { // … return FunctionNode }};createNodeByElement 函数源码引擎执行引擎执行其实就是访问链表,通过 visit 函数是最佳手段。const visit = tailCallOptimize( ({ node, store, visiterOption, childIndex }: { node: Node; store: VisiterStore; visiterOption: VisiterOption; childIndex: number; }) => { if (node instanceof ChainNode) { // 调用 visitChildNode 访问子节点 } else if (node instanceof TreeNode) { // 调用 visitChildNode 访问子节点 visitChildNode({ node, store, visiterOption, childIndex }); } else if (node instanceof MatchNode) { // 与当前 Token 进行匹配,匹配成功则调用 visitNextNodeFromParent 访问父级 Node 的下一个节点,匹配失败则调用 tryChances,这会在 “或” 逻辑里说明。 } else if (node instanceof FunctionNode) { // 执行函数节点,并替换掉当前节点,重新 visit 一遍 } });由于 visit 函数执行次数至多可能几百万次,因此使用 tailCallOptimize 进行尾递归优化,防止内存或堆栈溢出。visit 函数只负责访问节点本身,而 visitChildNode 函数负责访问节点的子节点(如果有),而 visitNextNodeFromParent 函数负责在没有子节点时,找到父级节点的下一个子节点访问。function visitChildNode({ node, store, visiterOption, childIndex}: { node: ParentNode; store: VisiterStore; visiterOption: VisiterOption; childIndex: number;}) { if (node instanceof ChainNode) { const child = node.childs[childIndex]; if (child) { // 调用 visit 函数访问子节点 child } else { // 如果没有子节点,就调用 visitNextNodeFromParent 往上找了 } } else { // 对于 TreeNode,如果不是访问到了最后一个节点,则添加一次 “存档” // 调用 addChances // 同时如果有子元素,visit 这个子元素 }}const visitNextNodeFromParent = tailCallOptimize( ( node: Node, store: VisiterStore, visiterOption: VisiterOption, astValue: any ) => { if (!node.parentNode) { // 找父节点的函数没有父级时,下面再介绍,记住这个位置叫 END 位。 } if (node.parentNode instanceof ChainNode) { // A B <- next node C // └── node <- current node // 正如图所示,找到 nextNode 节点调用 visit } else if (node.parentNode instanceof TreeNode) { // TreeNode 节点直接利用 visitNextNodeFromParent 跳过。因为同一时间 TreeNode 节点只有一个分支生效,所以它没有子元素了 } });可以看到 visitChildNode 与 visitNextNodeFromParent 函数都只处理好了自己的事情,而将其他工作交给别的函数完成,这样函数间职责分明,代码也更易懂。有了 vist visitChildNode 与 visitNextNodeFromParent,就完成了节点的访问、子节点的访问、以及当没有子节点时,追溯到上层节点的访问。visit 函数源码何时算执行完当 visitNextNodeFromParent 函数访问到 END 位 时,是时候做一个了结了:当 Tokens 正好消耗完,完美匹配成功。Tokens 没消耗完,匹配失败。还有一种失败情况,是 Chance 用光时,结合下面的 “或” 逻辑一起说。“或” 逻辑的实现“或” 逻辑是重构 JS 引擎的原因,现在这个问题被很好解决掉了。const main = () => chain(functionA, [functionB1, functionB2], functionC)();比如上面的代码,当遇到 [] 数组结构时,被认为是 “或” 逻辑,子元素存储在 TreeNode 节点中。在 visitChildNode 函数中,与 ChainNode 不同之处在于,访问 TreeNode 子节点时,还会调用 addChances 方法,为下一个子元素存储执行状态,以便未来恢复到这个节点继续执行。addChances 维护了一个池子,调用是先进后出:function addChances(/* … */) { const chance = { node, tokenIndex, childIndex }; store.restChances.push(chance);}与 addChance 相对的就是 tryChance。下面两种情况会调用 tryChances:MatchNode 匹配失败。节点匹配失败是最常见的失败情况,但如果 chances 池还有存档,就可以恢复过去继续尝试。没有下一个节点了,但 Tokens 还没消耗完,也说明匹配失败了,此时调用 tryChances 继续尝试。我们看看神奇的存档回复函数 tryChances 是如何做的:function tryChances( node: Node, store: VisiterStore, visiterOption: VisiterOption) { if (store.restChances.length === 0) { // 直接失败 } const nextChance = store.restChances.pop(); // reset scanner index store.scanner.setIndex(nextChance.tokenIndex); visit({ node: nextChance.node, store, visiterOption, childIndex: nextChance.childIndex });}tryChances 其实很简单,除了没有 chances 就失败外,找到最近的一个 chance 节点,恢复 Token 指针位置并 visit 这个节点就等价于读档。addChance 源码tryChances 源码many, optional, plus 的实现这三个方法实现的也很精妙。先看可选函数 optional:export const optional = (…elements: IElements) => { return chain([chain(…elements)(//)), true])(//);};可以看到,可选参数实际上就是一个 TreeNode,也就是:chain(optional(“a”))();// 等价于chain([“a”, true])();为什么呢?因为当 ‘a’ 匹配失败后,true 是一个不消耗 Token 一定成功的匹配,整体来看就是 “可选” 的意思。进一步解释下,如果 ‘a’ 没有匹配上,则 true 一定能匹配上,匹配 true 等于什么都没匹配,就等同于这个表达式不存在。再看匹配一或多个的函数 plus:export const plus = (…elements: IElements) => { const plusFunction = () => chain(chain(…elements)(//), optional(plusFunction))(//); return plusFunction;};能看出来吗?plus 函数等价于一个新递归函数。也就是:const aPlus = () => chain(plus(“a”))();// 等价于const aPlus = () => chain(plusFunc)();const plusFunc = () => chain(“a”, optional(plusFunc))();通过不断递归自身的方式匹配到尽可能多的元素,而每一层的 optional 保证了任意一层匹配失败后可以及时跳到下一个文法,不会失败。最后看匹配多个的函数 many:export const many = (…elements: IElements) => { return optional(plus(…elements));};many 就是 optional 的 plus,不是吗?这三个神奇的函数都利用了已有功能实现,建议每个函数留一分钟左右时间思考为什么。optional plus many 函数源码错误提示 & 输入推荐错误提示与输入推荐类似,都是给出错误位置或光标位置后期待的输入。输入推荐,就是给定字符串与光标位置,给出光标后期待内容的功能。首先通过光标位置找到光标的 上一个 Token,再通过 findNextMatchNodes 找到这个 Token 后所有可能匹配到的 MatchNode,这就是推荐结果。那么如何实现 findNextMatchNodes 呢?看下面:function findNextMatchNodes(node: Node, parser: Parser): MatchNode[] { const nextMatchNodes: MatchNode[] = []; let passCurrentNode = false; const visiterOption: VisiterOption = { onMatchNode: (matchNode, store, currentVisiterOption) => { if (matchNode === node && passCurrentNode === false) { passCurrentNode = true; // 调用 visitNextNodeFromParent,忽略自身 } else { // 遍历到的 MatchNode nextMatchNodes.push(matchNode); } // 这个是画龙点睛的一笔,所有推荐都当作匹配失败,通过 tryChances 可以找到所有可能的 MatchNode tryChances(matchNode, store, currentVisiterOption); } }; newVisit({ node, scanner: new Scanner([]), visiterOption, parser }); return nextMatchNodes;}所谓找到后续节点,就是通过 Visit 找到所有的 MatchNode,而 MatchNode 只要匹配一次即可,因为我们只要找到第一层级的 MatchNode。通过每次匹配后执行 tryChances,就可以找到所有 MatchNode 节点了!再看错误提示,我们要记录最后出错的位置,再采用输入推荐即可。但光标所在的位置是期望输入点,这个输入点也应该参与语法树的生成,而错误提示不包含光标,所以我们要 执行两次 visit。举个例子:select | from b;| 是光标位置,此时语句内容是 select from b; 显然是错误的,但光标位置应该给出提示,给出提示就需要正确解析语法树,所以对于提示功能,我们需要将光标位置考虑进去一起解析。因此一共有两次解析。findNextMatchNodes 函数源码First 集优化构建 First 集是个自下而上的过程,当访问到 MatchNode 节点时,其值就是其父节点的一个 First 值,当父节点的 First 集收集完毕后,,就会触发它的父节点 First 集收集判断,如此递归,最后完成 First 集收集的是最顶级节点。篇幅原因,不再赘述,可以看 这张图。generateFirstSet 函数源码3. 总结这篇文章是对 《手写 SQL 编译器》 系列的总结,从源码角度的总结!该系列的每篇文章都以图文的方式介绍了各技术细节,可以作为补充阅读:精读《手写 SQL 编译器 - 词法分析》精读《手写 SQL 编译器 - 文法介绍》精读《手写 SQL 编译器 - 语法分析》精读《手写 SQL 编译器 - 回溯》精读《手写 SQL 编译器 - 语法树》精读《手写 SQL 编译器 - 错误提示》精读《手写 SQL 编译器 - 性能优化之缓存》精读《手写 SQL 编译器 - 智能提示》讨论地址是:精读《syntax-parser 源码》 · Issue #133 · dt-fe/weekly如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

March 4, 2019 · 6 min · jiezi

Go 语言编译过程概述

Golang 是一门需要编译才能运行的编程语言,也就说代码在运行之前需要通过编译器生成二进制机器码,随后二进制文件才能在目标机器上运行,如果我们想要了解 Go 语言的实现原理,理解它的编译过程就是一个没有办法绕过的事情。这一节会先对 Go 语言编译的过程进行概述,从顶层介绍编译器执行的几个步骤,随后的章节会分别剖析各个步骤完成的工作和实现原理,同时也会对一些需要预先掌握的知识进行介绍和准备,确保后面的章节能够被更好的理解。目录编译原理概述词法和语法分析器类型检查中间代码生成机器码生成预备知识想要深入了解 Go 语言的编译过程,需要提前了解一下编译过程中涉及的一些术语和专业知识。这些知识其实在我们的日常工作和学习中比较难用到,但是对于理解编译的过程和原理还是非常重要的。这一小节会简单挑选几个常见并且重要的概念提前进行介绍,减少后面章节的理解压力。抽象语法树抽象语法树(AST)是源代码语法的结构的一种抽象表示,它用树状的方式表示编程语言的语法结构。抽象语法树中的每一个节点都表示源代码中的一个元素,每一颗子树都表示一个语法元素,例如一个 if else 语句,我们可以从 2 * 3 + 7 这一表达式中解析出下图所示的抽象语法树。作为编译器常用的数据结构,抽象语法树抹去了源代码中不重要的一些字符 - 空格、分号或者括号等等。编译器在执行完语法分析之后会输出一个抽象语法树,这棵树会辅助编译器进行语义分析,我们可以用它来确定结构正确的程序是否存在一些类型不匹配或不一致的问题。静态单赋值静态单赋值(SSA)是中间代码的一个特性,如果一个中间代码具有静态单赋值的特性,那么每个变量就只会被赋值一次,在实践中我们通常会用添加下标的方式实现每个变量只能被赋值一次的特性,这里以下面的代码举一个简单的例子:x := 1x := 2y := x根据分析,我们其实能够发现上述的代码其实并不需要第一个将 1 赋值给 x 的表达式,也就是这一表达式在整个代码片段中是没有作用的:x1 := 1x2 := 2y1 := x2从使用 SSA 的『中间代码』我们就可以非常清晰地看出变量 y1 的值和 x1 是完全没有任何关系的,所以在机器码生成时其实就可以省略第一步,这样就能减少需要执行的指令来优化这一段代码。根据 Wikipedia 对 SSA 的介绍来看,在中间代码中使用 SSA 的特性能够为整个程序实现以下的优化:常数传播(constant propagation)值域传播(value range propagation)稀疏有条件的常数传播(sparse conditional constant propagation)消除无用的程式码(dead code elimination)全域数值编号(global value numbering)消除部分的冗余(partial redundancy elimination)强度折减(strength reduction)寄存器分配(register allocation)从 SSA 的作用我们就能看出,因为它的主要作用就是代码的优化,所以是编译器后端(主要负责目标代码的优化和生成)的一部分;当然,除了 SSA 之外代码编译领域还有非常多的中间代码优化方法,优化编译器生成的代码是一个非常古老并且复杂的领域,这里就不会展开介绍了。指令集架构最后要介绍的一个预备知识就是指令集的架构了,很多开发者都会遇到在生产环境运行的结果和本地不同的问题,导致这种情况的原因其实非常复杂,不同机器使用不同的指令也是可能的原因之一。我们大多数开发者都会使用 x86_64 的 Macbook 作为工作上主要使用的硬件,在命令行中输入 uname -m 就能够获得当前机器上硬件的信息:$ uname -mx86_64x86_64 是目前比较常见的指令集架构之一,除了 x86_64 之外,还包含其他类型的指令集架构,例如 amd64、arm64 以及 mips 等等,不同的处理器使用了大不相同的机器语言,所以很多编程语言为了在不同的机器上运行需要将源代码根据架构翻译成不同的机器代码。复杂指令集计算机(CISC)和精简指令集计算机(RISC)是目前的两种 CPU 区别,它们的在设计理念上会有一些不同,从名字我们就能看出来这两种不同的设计有什么区别,复杂指令集通过增加指令的数量减少需要执行的质量数,而精简指令集能使用更少的指令完成目标的计算任务;早期的 CPU 为了减少机器语言指令的数量使用复杂指令集完成计算任务,这两者之前的区别其实就是设计上的权衡,我们会在后面的章节 机器码生成 中详细介绍指令集架构,当然各位读者也可以自行搜索和学习。编译原理Go 语言编译器的源代码在 cmd/compile 目录中,目录下的文件共同构成了 Go 语言的编译器,学过编译原理的人可能听说过编译器的前端和后端,编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作,而编译器后端主要负责目标代码的生成和优化,也就是将中间代码翻译成目标『机器』能够运行的机器码。Go 的编译器在逻辑上可以被分成四个阶段:词法与语法分析、类型检查和 AST 转换、通用 SSA 生成和最后的机器代码生成,在这一节我们会使用比较少的篇幅分别介绍这四个阶段做的工作,后面的章节会具体介绍每一个阶段的具体内容。词法与语法分析所有的编译过程其实都是从解析代码的源文件开始的,词法分析的作用就是解析源代码文件,它将文件中的字符串序列转换成 Token 序列,方便后面的处理和解析,我们一般会把执行词法分析的程序称为词法解析器(lexer)。而语法分析的输入就是词法分析器输出的 Token 序列,这些序列会按照顺序被语法分析器进行解析,语法的解析过程就是将词法分析生成的 Token 按照语言定义好的文法(Grammar)自下而上或者自上而下的进行规约,每一个 Go 的源代码文件最终会被归纳成一个 SourceFile 结构:SourceFile = PackageClause “;” { ImportDecl “;” } { TopLevelDecl “;” } .标准的 Golang 语法解析器使用的就是 LALR(1) 的文法,语法解析的结果其实就是上面介绍过的抽象语法树(AST),每一个 AST 都对应着一个单独的 Go 语言文件,这个抽象语法树中包括当前文件属于的包名、定义的常量、结构体和函数等。如果在语法解析的过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止。我们会在这一章后面的小节 词法与语法分析 中介绍 Go 语言的文法和它的词法与语法解析过程。类型检查当拿到一组文件的抽象语法树 AST 之后,Go 语言的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,按照以下的顺序进行处理:常量、类型和函数名及类型;变量的赋值和初始化;函数和闭包的主体;哈希键值对的类型;导入函数体;外部的声明;通过对每一棵抽象节点树的遍历,我们在每一个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在这一个阶段被发现和暴露出来。类型检查的阶段不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写,例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数。我们其实能够看出类型检查不止做了验证类型的工作,还做了对 AST 进行改写,处理 Go 语言内置关键字的活,所以,这一过程在整个编译流程中还是非常重要的,没有这个步骤很多关键字其实就没有办法工作,后面的章节 类型检查 会介绍这一步骤。中间代码生成当我们将源文件转换成了抽象语法树、对整棵树的语法进行解析并进行类型检查之后,就可以认为当前文件中的代码基本上不存在无法编译或者语法错误的问题了,Go 语言的编译器就会将输入的 AST 转换成中间代码。Go 语言编译器的中间代码使用了 SSA(Static Single Assignment Form) 的特性,如果我们在中间代码生成的过程中使用这种特性,就能够比较容易的分析出代码中的无用变量和片段并对代码进行优化。在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个 Go 语言项目中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作协程的消费,这些 Goroutine 会将所有函数对应的 AST 转换成使用 SSA 特性的中间代码。中间代码生成 这一章节会详细介绍中间代码的生成过程并简单介绍 Golang 是如何在中间代码中使用 SSA 的特性的,在这里就不展开介绍其他的内容了。机器码生成Go 语言源代码的 cmd/compile/internal 中包含了非常多机器码生成相关的包,不同类型的 CPU 分别使用了不同的包进行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm,也就是说 Go 语言能够在上述的 CPU 指令集类型上运行,其中比较有趣的就是 WebAssembly 了。作为一种在栈虚拟机上使用的二进制指令格式,它的设计的主要目标就是在 Web 浏览器上提供一种具有高可移植性的目标语言。Go 语言的编译器既然能够生成 WASM 格式的指令,那么就能够运行在常见的主流浏览器中。$ GOARCH=wasm GOOS=js go build -o lib.wasm main.go我们可以使用上述的命令将 Go 的源代码编译成能够在浏览器上运行的『汇编语言』,除了这种新兴的指令之外,Go 语言还支持了几乎全部常见的 CPU 指令集类型,也就是说它编译出的机器码能够在使用上述指令集的机器上运行。机器码生成 一节会详细介绍将中间代码翻译到不同目标机器的过程,在这个章节中也会简单介绍不同的指令集架构的区别。编译器入口Go 语言的编译器入口在 src/cmd/compile/internal/pc 包中的 main.go 文件,这个 600 多行的 Main 函数就是 Go 语言编译器的主程序,这个函数会先获取命令行传入的参数并更新编译的选项和配置,随后就会开始运行 parseFiles 函数对输入的所有文件进行词法与语法分析得到文件对应的抽象语法树:func Main(archInit func(*Arch)) { // … lines := parseFiles(flag.Args())接下来就会分九个阶段对抽象语法树进行更新和编译,就像我们在上面介绍的,整个过程会经历类型检查、SSA 中间代码生成以及机器码生成三个部分:检查常量、类型和函数的类型;处理变量的赋值;对函数的主体进行类型检查;决定如何捕获变量;检查内联函数的类型;进行逃逸分析;将闭包的主体转换成引用的捕获变量;编译顶层函数;检查外部依赖的声明;了解了剩下的编译过程之后,我们重新回到词法和语法分析后的具体流程,在这里编译器会对生成语法树中的节点执行类型检查,除了常量、类型和函数这些顶层声明之外,它还会对变量的赋值语句、函数主体等结构进行检查: for i := 0; i < len(xtop); i++ { n := xtop[i] if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias) { xtop[i] = typecheck(n, ctxStmt) } } for i := 0; i < len(xtop); i++ { n := xtop[i] if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias { xtop[i] = typecheck(n, ctxStmt) } } for i := 0; i < len(xtop); i++ { n := xtop[i] if op := n.Op; op == ODCLFUNC || op == OCLOSURE { typecheckslice(Curfn.Nbody.Slice(), ctxStmt) } } checkMapKeys() for _, n := range xtop { if n.Op == ODCLFUNC && n.Func.Closure != nil { capturevars(n) } } escapes(xtop) for _, n := range xtop { if n.Op == ODCLFUNC && n.Func.Closure != nil { transformclosure(n) } }类型检查会对传入节点的子节点进行遍历,这个过程会对 make 等关键字进行展开和重写,类型检查结束之后并没有输出新的数据结构,只是改变了语法树中的一些节点,同时这个过程的结束也意味着源代码中已经不存在语法错误和类型错误,中间代码和机器码也都可以正常的生成了。 initssaconfig() peekitabs() for i := 0; i < len(xtop); i++ { n := xtop[i] if n.Op == ODCLFUNC { funccompile(n) } } compileFunctions() for i, n := range externdcl { if n.Op == ONAME { externdcl[i] = typecheck(externdcl[i], ctxExpr) } } checkMapKeys()}在主程序运行的最后,会将顶层的函数编译成中间代码并根据目标的 CPU 架构生成机器码,不过这里其实也可能会再次对外部依赖进行类型检查以验证正确性。总结Go 语言的编译过程其实是非常有趣并且值得学习的,通过对 Go 语言四个编译阶段的分析和对编译器主函数的梳理,我们能够对 Golang 的实现有一些基本的理解,掌握编译的过程之后,Go 语言对于我们来讲也不再是一个黑盒,所以学习其编译原理的过程还是非常让人着迷的。相关文章编译原理概述词法和语法分析器类型检查中间代码生成机器码生成ReferenceIntroduction to the Go compilerGo 1.5 Bootstrap PlanGo grammar questiowhat type of grammar GO programming language? ...

February 11, 2019 · 3 min · jiezi

温故而知新:JS 变量提升与时间死区

开始执行脚本时,执行脚本的第一步是编译代码,然后再开始执行代码,如图另外,在编译优化方面来说,最开始时也并不是全部编译好脚本,而是当函数执行时,才会先编译,再执行脚本,如图编译阶段:经历了词法分析,语法分析生成AST,以及代码生成。并且在此阶段,它只会扫描并且抽出环境中的声明变量,声明函数以便准备分配内存,所有的函数声明和变量声明都会被添加到名为Lexical Environment的JavaScript内部数据结构内的内存中。因此,它们可以在源代码中实际声明之前使用。但是,Javascript只会存储函数声明和变量声明在内存,并不会存储他们的值执行阶段:给变量x赋值,首先询问内存你这有变量x吗,如果有,则给变量x赋值,如果没有则创建变量x并且给它赋值。变量提升如下图,左边灰色块区域,是演示函数执行前的编译阶段,先抽出所有声明变量和声明函数,并进行内存分配。然后再开始执行代码,在执行第一行代码的时候,若是变量a存在于内存中,则直接给变量a赋值。而执行到第二行时,变量b并没有在内存中,则会创建变量b并给它赋值。Lexical enviroment是一种包含标识符变量映射的数据结构LexicalEnviroment = { Identifier: <value>, Indentifier: <function object>}简而言之,Lexical enviroment就是程序执行过程中变量和函数存在的地方。let,const变量console.log(a)let a = 3;输出ReferenceError: a is not defined所以let和const变量并不会被提升吗?这个答案会比较复杂。所有的声明(function, var, let, const and class)在JavaScript中都会被提升,然而var声明被undefined值初始化,但是let和const声明的值仍然未被初始化。它们仅仅只在Javascript引擎运行期间它们的词法绑定被执行在才会被初始化。这意味着引擎在源代码中声明它的位置计算其值之前,你无法访问该变量。这就是我们所说的时间死区,即变量创建和初始化之间的时间,我们无法访问该变量。如果JavaScript引擎仍然无法在声明它们的行中找到let或者const的值,它将为它们分配undefined值或返回错误值(在const的情况下会返回错误值)。6a9a50532bf60f5fac6b3c.png](evernotecid://F2BCA3B5-CC5A-4EB3-BD61-DD865800F342/appyinxiangcom/10369121/ENResource/p1163)let a;console.log(a); // outputs undefineda = 5;在编译阶段,JavaScript引擎遇到变量a并将它存储在lexical enviroment,但是因为它是一个let变量,所以引擎不会为它初始化任何值。所以,在编译阶段,lexical enviroment看起来像下面这样。// 编译阶段lexicalEnvironment = { a: <uninitialized>}现在如果我们尝试在声明它之前访问该变量,JavaScript引擎将会尝试从词法环境中拿到这个变量的值,因为这个变量未被初始化,它将抛出一个引用错误。在执行期间,当引擎到达了变量声明的行,它将试图执行它的绑定,因为该变量没有与之关联的值,因此它将为其赋值为unedfined// 执行阶段lexicalEnviroment = { a: undefined}之后,undefined将会被打印到控制台,然后将值5赋值给变量a,lexical enviroment中变量a的值也会从undefined更新为5functionn foo() { console.log(a)}let a = 20;foo(); function foo() { console.log(a): // ReferenceError: a is not defined}foo();let a = 20;Class Declaration就像let和const声明一样,class在JavaScript中也会被提升,并且和let,const一样,知道执行之前,它们都会保持uninitialized。因此它们同样会受到Temporal Deal Zone(时间死区)的影响。例如let peter = new Person(‘Peter’, 25); // ReferenceError: Person is not definedconsole.log(peter);class Person { constructor(name, age) { this.name = name; this.age = age; }}因此要访问class,必须先声明它class Person { constructor(name, age) { this.name = name; this.age = age; }}let peter = new Person(‘Peter’, 25); console.log(peter);// Person { name: ‘Peter’, age: 25 }所以在编译阶段,上面代码的lexical environment(词法环境)将如下所示:lexicalEnvironment = { Person: <uninitialized>}当引擎执行class声明时,它将使用值初始化类。lexicalEnvironment = { Person: <Person object>}提升Class Expressionslet peter = new Person(‘Peter’, 25);console.log(peter);let Person = class { constructor(name, age) { this.name = name; this.age = age; }}let peter = new Person(‘Peter’, 25); console.log(peter);var Person = class { constructor(name, age) { this.name = name; this.age = age; }}所以现在我们知道在提升过程中我们的代码并没有被JavaScript引擎实际移动。正确理解提升机制将有助于避免因变量提升而产生的任何未来错误和混乱。为了避免像未定义的变量或引用错误一样可能产生的副作用,请始终尝试将变量声明在各自作用域的顶部,并始终尝试在声明变量时初始化变量。Hoisting in Modern JavaScript — let, const, and var ...

January 27, 2019 · 1 min · jiezi

如何学习编译原理

对于没有计算机科学基础的程序员或初学者来说 一上来就看龙书 虎书是行不通的 全是理论知识 看得想睡觉 我还试过看网易云大学计算机专业的编译原理课程 也是看得一头雾水 看到80多讲就看不下去了 另外 LISP(计算机程序的构造和解释)这本很多人推荐的书其实并不适合初学者 前3章和后面几章难度差别有点大 可能是自己水平不行 看LISP解释器和编译器那两章也是看不懂 虽然强迫自己看完 但是最后还是不懂编译原理到底是怎么回事不过 后来我还是通过学习一本书的知识 写出来了一个简单的编译器这本书简单 通俗易懂 对计算机体系知识有一个较全面的介绍 而你只需要会一门编译语言就行了 它就是《计算机系统要素》这本书前面5章讲的是硬件知识 虽然跟编译原理没什么关系 但是对于了解计算机硬件知识是很有用的 重点是通俗易懂后面的章节就是和编译原理有关的知识了书里的内容介绍了汇编编译器(将汇编语言翻译为机器语言)VM编译器(将虚拟机语言翻译为汇编语言)编译器(将高级语言翻译为虚拟机语言)不要看到有3个编译器就觉得难 其实相对于上面介绍的书籍 算是非常简单了我大概花了1个多月的时间完成了这本书的所有项目 最终写出了一个编译器 算是对编译原理有了一个比较全面但不深入的了解吧 这个时候再去看龙书 虎书 就不会感觉很吃力了附上我完成这本书所有项目的答案https://github.com/woai3c/nan…再最后说一句 这本书的内容真的是通俗易懂!通俗易懂!通俗易懂!

January 11, 2019 · 1 min · jiezi

前端与编译原理——用JS写一个JS解释器

说起编译原理,印象往往只停留在本科时那些枯燥的课程和晦涩的概念。作为前端开发者,编译原理似乎离我们很远,对它的理解很可能仅仅局限于“抽象语法树(AST)”。但这仅仅是个开头而已。编译原理的使用,甚至能让我们利用JS直接写一个能运行JS代码的解释器。项目地址:https://github.com/jrainlau/c…在线体验:https://codepen.io/jrainlau/p…为什么要用JS写JS的解释器接触过小程序开发的同学应该知道,小程序运行的环境禁止new Function,eval等方法的使用,导致我们无法直接执行字符串形式的动态代码。此外,许多平台也对这些JS自带的可执行动态代码的方法进行了限制,那么我们是没有任何办法了吗?既然如此,我们便可以用JS写一个解析器,让JS自己去运行自己。在开始之前,我们先简单回顾一下编译原理的一些概念。什么是编译器说到编译原理,肯定离不开编译器。简单来说,当一段代码经过编译器的词法分析、语法分析等阶段之后,会生成一个树状结构的“抽象语法树(AST)”,该语法树的每一个节点都对应着代码当中不同含义的片段。比如有这么一段代码:const a = 1console.log(a)经过编译器处理后,它的AST长这样:{ “type”: “Program”, “start”: 0, “end”: 26, “body”: [ { “type”: “VariableDeclaration”, “start”: 0, “end”: 11, “declarations”: [ { “type”: “VariableDeclarator”, “start”: 6, “end”: 11, “id”: { “type”: “Identifier”, “start”: 6, “end”: 7, “name”: “a” }, “init”: { “type”: “Literal”, “start”: 10, “end”: 11, “value”: 1, “raw”: “1” } } ], “kind”: “const” }, { “type”: “ExpressionStatement”, “start”: 12, “end”: 26, “expression”: { “type”: “CallExpression”, “start”: 12, “end”: 26, “callee”: { “type”: “MemberExpression”, “start”: 12, “end”: 23, “object”: { “type”: “Identifier”, “start”: 12, “end”: 19, “name”: “console” }, “property”: { “type”: “Identifier”, “start”: 20, “end”: 23, “name”: “log” }, “computed”: false }, “arguments”: [ { “type”: “Identifier”, “start”: 24, “end”: 25, “name”: “a” } ] } } ], “sourceType”: “module”}常见的JS编译器有babylon,acorn等等,感兴趣的同学可以在AST explorer这个网站自行体验。可以看到,编译出来的AST详细记录了代码中所有语义代码的类型、起始位置等信息。这段代码除了根节点Program外,主体包含了两个节点VariableDeclaration和ExpressionStatement,而这些节点里面又包含了不同的子节点。正是由于AST详细记录了代码的语义化信息,所以Babel,Webpack,Sass,Less等工具可以针对代码进行非常智能的处理。什么是解释器如同翻译人员不仅能看懂一门外语,也能对其艺术加工后把它翻译成母语一样,人们把能够将代码转化成AST的工具叫做“编译器”,而把能够将AST翻译成目标语言并运行的工具叫做“解释器”。在编译原理的课程中,我们思考过这么一个问题:如何让计算机运行算数表达式1+2+3:1 + 2 + 3当机器执行的时候,它可能会是这样的机器码:1 PUSH 12 PUSH 23 ADD4 PUSH 35 ADD而运行这段机器码的程序,就是解释器。在这篇文章中,我们不会搞出机器码这样复杂的东西,仅仅是使用JS在其runtime环境下去解释JS代码的AST。由于解释器使用JS编写,所以我们可以大胆使用JS自身的语言特性,比如this绑定、new关键字等等,完全不需要对它们进行额外处理,也因此让JS解释器的实现变得非常简单。在回顾了编译原理的基本概念之后,我们就可以着手进行开发了。节点遍历器通过分析上文的AST,可以看到每一个节点都会有一个类型属性type,不同类型的节点需要不同的处理方式,处理这些节点的程序,就是“节点处理器(nodeHandler)”定义一个节点处理器:const nodeHandler = { Program () {}, VariableDeclaration () {}, ExpressionStatement () {}, MemberExpression () {}, CallExpression () {}, Identifier () {}}关于节点处理器的具体实现,会在后文进行详细探讨,这里暂时不作展开。有了节点处理器,我们便需要去遍历AST当中的每一个节点,递归地调用节点处理器,直到完成对整棵语法书的处理。定义一个节点遍历器(NodeIterator):class NodeIterator { constructor (node) { this.node = node this.nodeHandler = nodeHandler } traverse (node) { // 根据节点类型找到节点处理器当中对应的函数 const _eval = this.nodeHandler[node.type] // 若找不到则报错 if (!_eval) { throw new Error(canjs: Unknown node type "${node.type}".) } // 运行处理函数 return _eval(node) }}理论上,节点遍历器这样设计就可以了,但仔细推敲,发现漏了一个很重要的东西——作用域处理。回到节点处理器的VariableDeclaration()方法,它用来处理诸如const a = 1这样的变量声明节点。假设它的代码如下: VariableDeclaration (node) { for (const declaration of node.declarations) { const { name } = declaration.id const value = declaration.init ? traverse(declaration.init) : undefined // 问题来了,拿到了变量的名称和值,然后把它保存到哪里去呢? // … } },问题在于,处理完变量声明节点以后,理应把这个变量保存起来。按照JS语言特性,这个变量应该存放在一个作用域当中。在JS解析器的实现过程中,这个作用域可以被定义为一个scope对象。改写节点遍历器,为其新增一个scope对象class NodeIterator { constructor (node, scope = {}) { this.node = node this.scope = scope this.nodeHandler = nodeHandler } traverse (node, options = {}) { const scope = options.scope || this.scope const nodeIterator = new NodeIterator(node, scope) const _eval = this.nodeHandler[node.type] if (!_eval) { throw new Error(canjs: Unknown node type "${node.type}".) } return _eval(nodeIterator) } createScope (blockType = ‘block’) { return new Scope(blockType, this.scope) }}然后节点处理函数VariableDeclaration()就可以通过scope保存变量了: VariableDeclaration (nodeIterator) { const kind = nodeIterator.node.kind for (const declaration of nodeIterator.node.declarations) { const { name } = declaration.id const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined // 在作用域当中定义变量 // 如果当前是块级作用域且变量用var定义,则定义到父级作用域 if (nodeIterator.scope.type === ‘block’ && kind === ‘var’) { nodeIterator.scope.parentScope.declare(name, value, kind) } else { nodeIterator.scope.declare(name, value, kind) } } },关于作用域的处理,可以说是整个JS解释器最难的部分。接下来我们将对作用域处理进行深入的剖析。作用域处理考虑到这样一种情况:const a = 1{ const b = 2 console.log(a)}console.log(b)运行结果必然是能够打印出a的值,然后报错:Uncaught ReferenceError: b is not defined这段代码就是涉及到了作用域的问题。块级作用域或者函数作用域可以读取其父级作用域当中的变量,反之则不行,所以对于作用域我们不能简单地定义一个空对象,而是要专门进行处理。定义一个作用域基类Scope:class Scope { constructor (type, parentScope) { // 作用域类型,区分函数作用域function和块级作用域block this.type = type // 父级作用域 this.parentScope = parentScope // 全局作用域 this.globalDeclaration = standardMap // 当前作用域的变量空间 this.declaration = Object.create(null) } /* * get/set方法用于获取/设置当前作用域中对应name的变量值 符合JS语法规则,优先从当前作用域去找,若找不到则到父级作用域去找,然后到全局作用域找。 如果都没有,就报错 / get (name) { if (this.declaration[name]) { return this.declaration[name] } else if (this.parentScope) { return this.parentScope.get(name) } else if (this.globalDeclaration[name]) { return this.globalDeclaration[name] } throw new ReferenceError(${name} is not defined) } set (name, value) { if (this.declaration[name]) { this.declaration[name] = value } else if (this.parentScope[name]) { this.parentScope.set(name, value) } else { throw new ReferenceError(${name} is not defined) } } /* * 根据变量的kind调用不同的变量定义方法 / declare (name, value, kind = ‘var’) { if (kind === ‘var’) { return this.varDeclare(name, value) } else if (kind === ’let’) { return this.letDeclare(name, value) } else if (kind === ‘const’) { return this.constDeclare(name, value) } else { throw new Error(canjs: Invalid Variable Declaration Kind of "${kind}") } } varDeclare (name, value) { let scope = this // 若当前作用域存在非函数类型的父级作用域时,就把变量定义到父级作用域 while (scope.parentScope && scope.type !== ‘function’) { scope = scope.parentScope } this.declaration[name] = new SimpleValue(value, ‘var’) return this.declaration[name] } letDeclare (name, value) { // 不允许重复定义 if (this.declaration[name]) { throw new SyntaxError(Identifier ${name} has already been declared) } this.declaration[name] = new SimpleValue(value, ’let’) return this.declaration[name] } constDeclare (name, value) { // 不允许重复定义 if (this.declaration[name]) { throw new SyntaxError(Identifier ${name} has already been declared) } this.declaration[name] = new SimpleValue(value, ‘const’) return this.declaration[name] }}这里使用了一个叫做simpleValue()的函数来定义变量值,主要用于处理常量:class SimpleValue { constructor (value, kind = ‘’) { this.value = value this.kind = kind } set (value) { // 禁止重新对const类型变量赋值 if (this.kind === ‘const’) { throw new TypeError(‘Assignment to constant variable’) } else { this.value = value } } get () { return this.value }}处理作用域问题思路,关键的地方就是在于JS语言本身寻找变量的特性——优先当前作用域,父作用域次之,全局作用域最后。反过来,在节点处理函数VariableDeclaration()里,如果遇到块级作用域且关键字为var,则需要把这个变量也定义到父级作用域当中,这也就是我们常说的“全局变量污染”。JS标准库注入细心的读者会发现,在定义Scope基类的时候,其全局作用域globalScope被赋值了一个standardMap对象,这个对象就是JS标准库。简单来说,JS标准库就是JS这门语言本身所带有的一系列方法和属性,如常用的setTimeout,console.log等等。为了让解析器也能够执行这些方法,所以我们需要为其注入标准库:const standardMap = { console: new SimpleValue(console)}这样就相当于往解析器的全局作用域当中注入了console这个对象,也就可以直接被使用了。节点处理器在处理完节点遍历器、作用域处理的工作之后,便可以来编写节点处理器了。顾名思义,节点处理器是专门用来处理AST节点的,上文反复提及的VariableDeclaration()方法便是其中一个。下面将对部分关键的节点处理器进行讲解。在开发节点处理器之前,需要用到一个工具,用于判断JS语句当中的return,break,continue关键字。关键字判断工具Signal定义一个Signal基类:class Signal { constructor (type, value) { this.type = type this.value = value } static Return (value) { return new Signal(‘return’, value) } static Break (label = null) { return new Signal(‘break’, label) } static Continue (label) { return new Signal(‘continue’, label) } static isReturn(signal) { return signal instanceof Signal && signal.type === ‘return’ } static isContinue(signal) { return signal instanceof Signal && signal.type === ‘continue’ } static isBreak(signal) { return signal instanceof Signal && signal.type === ‘break’ } static isSignal (signal) { return signal instanceof Signal }}有了它,就可以对语句当中的关键字进行判断处理,接下来会有大用处。变量定义节点处理器——VariableDeclaration()最常用的节点处理器之一,负责把变量注册到正确的作用域。 VariableDeclaration (nodeIterator) { const kind = nodeIterator.node.kind for (const declaration of nodeIterator.node.declarations) { const { name } = declaration.id const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined // 在作用域当中定义变量 // 若为块级作用域且关键字为var,则需要做全局污染 if (nodeIterator.scope.type === ‘block’ && kind === ‘var’) { nodeIterator.scope.parentScope.declare(name, value, kind) } else { nodeIterator.scope.declare(name, value, kind) } } },标识符节点处理器——Identifier()专门用于从作用域中获取标识符的值。 Identifier (nodeIterator) { if (nodeIterator.node.name === ‘undefined’) { return undefined } return nodeIterator.scope.get(nodeIterator.node.name).value },字符节点处理器——Literal()返回字符节点的值。 Literal (nodeIterator) { return nodeIterator.node.value }表达式调用节点处理器——CallExpression()用于处理表达式调用节点的处理器,如处理func(),console.log()等。 CallExpression (nodeIterator) { // 遍历callee获取函数体 const func = nodeIterator.traverse(nodeIterator.node.callee) // 获取参数 const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg)) let value if (nodeIterator.node.callee.type === ‘MemberExpression’) { value = nodeIterator.traverse(nodeIterator.node.callee.object) } // 返回函数运行结果 return func.apply(value, args) },表达式节点处理器——MemberExpression()区分于上面的“表达式调用节点处理器”,表达式节点指的是person.say,console.log这种函数表达式。 MemberExpression (nodeIterator) { // 获取对象,如console const obj = nodeIterator.traverse(nodeIterator.node.object) // 获取对象的方法,如log const name = nodeIterator.node.property.name // 返回表达式,如console.log return obj[name] }块级声明节点处理器——BlockStatement()非常常用的处理器,专门用于处理块级声明节点,如函数、循环、try…catch…当中的情景。 BlockStatement (nodeIterator) { // 先定义一个块级作用域 let scope = nodeIterator.createScope(‘block’) // 处理块级节点内的每一个节点 for (const node of nodeIterator.node.body) { if (node.type === ‘VariableDeclaration’ && node.kind === ‘var’) { for (const declaration of node.declarations) { scope.declare(declaration.id.name, declaration.init.value, node.kind) } } else if (node.type === ‘FunctionDeclaration’) { nodeIterator.traverse(node, { scope }) } } // 提取关键字(return, break, continue) for (const node of nodeIterator.node.body) { if (node.type === ‘FunctionDeclaration’) { continue } const signal = nodeIterator.traverse(node, { scope }) if (Signal.isSignal(signal)) { return signal } } }可以看到这个处理器里面有两个for…of循环。第一个用于处理块级内语句,第二个专门用于识别关键字,如循环体内部的break,continue或者函数体内部的return。函数定义节点处理器——FunctionDeclaration()往作用当中声明一个和函数名相同的变量,值为所定义的函数: FunctionDeclaration (nodeIterator) { const fn = NodeHandler.FunctionExpression(nodeIterator) nodeIterator.scope.varDeclare(nodeIterator.node.id.name, fn) return fn }函数表达式节点处理器——FunctionExpression()用于定义一个函数: FunctionExpression (nodeIterator) { const node = nodeIterator.node /* * 1、定义函数需要先为其定义一个函数作用域,且允许继承父级作用域 * 2、注册this, arguments和形参到作用域的变量空间 * 3、检查return关键字 * 4、定义函数名和长度 */ const fn = function () { const scope = nodeIterator.createScope(‘function’) scope.constDeclare(’this’, this) scope.constDeclare(‘arguments’, arguments) node.params.forEach((param, index) => { const name = param.name scope.varDeclare(name, arguments[index]) }) const signal = nodeIterator.traverse(node.body, { scope }) if (Signal.isReturn(signal)) { return signal.value } } Object.defineProperties(fn, { name: { value: node.id ? node.id.name : ’’ }, length: { value: node.params.length } }) return fn }this表达式处理器——ThisExpression()该处理器直接使用JS语言自身的特性,把this关键字从作用域中取出即可。 ThisExpression (nodeIterator) { const value = nodeIterator.scope.get(’this’) return value ? value.value : null }new表达式处理器——NewExpression()和this表达式类似,也是直接沿用JS的语言特性,获取函数和参数之后,通过bind关键字生成一个构造函数,并返回。 NewExpression (nodeIterator) { const func = nodeIterator.traverse(nodeIterator.node.callee) const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg)) return new (func.bind(null, …args)) }For循环节点处理器——ForStatement()For循环的三个参数对应着节点的init,test,update属性,对着三个属性分别调用节点处理器处理,并放回JS原生的for循环当中即可。 ForStatement (nodeIterator) { const node = nodeIterator.node let scope = nodeIterator.scope if (node.init && node.init.type === ‘VariableDeclaration’ && node.init.kind !== ‘var’) { scope = nodeIterator.createScope(‘block’) } for ( node.init && nodeIterator.traverse(node.init, { scope }); node.test ? nodeIterator.traverse(node.test, { scope }) : true; node.update && nodeIterator.traverse(node.update, { scope }) ) { const signal = nodeIterator.traverse(node.body, { scope }) if (Signal.isBreak(signal)) { break } else if (Signal.isContinue(signal)) { continue } else if (Signal.isReturn(signal)) { return signal } } }同理,for…in,while和do…while循环也是类似的处理方式,这里不再赘述。If声明节点处理器——IfStatemtnt()处理If语句,包括if,if…else,if…elseif…else。 IfStatement (nodeIterator) { if (nodeIterator.traverse(nodeIterator.node.test)) { return nodeIterator.traverse(nodeIterator.node.consequent) } else if (nodeIterator.node.alternate) { return nodeIterator.traverse(nodeIterator.node.alternate) } }同理,switch语句、三目表达式也是类似的处理方式。—上面列出了几个比较重要的节点处理器,在es5当中还有很多节点需要处理,详细内容可以访问这个地址一探究竟。定义调用方式经过了上面的所有步骤,解析器已经具备处理es5代码的能力,接下来就是对这些散装的内容进行组装,最终定义一个方便用户调用的办法。const { Parser } = require(‘acorn’)const NodeIterator = require(’./iterator’)const Scope = require(’./scope’)class Canjs { constructor (code = ‘’, extraDeclaration = {}) { this.code = code this.extraDeclaration = extraDeclaration this.ast = Parser.parse(code) this.nodeIterator = null this.init() } init () { // 定义全局作用域,该作用域类型为函数作用域 const globalScope = new Scope(‘function’) // 根据入参定义标准库之外的全局变量 Object.keys(this.extraDeclaration).forEach((key) => { globalScope.addDeclaration(key, this.extraDeclaration[key]) }) this.nodeIterator = new NodeIterator(null, globalScope) } run () { return this.nodeIterator.traverse(this.ast) }}这里我们定义了一个名为Canjs的基类,接受字符串形式的JS代码,同时可定义标准库之外的变量。当运行run()方法的时候就可以得到运行结果。后续至此,整个JS解析器已经完成,可以很好地运行ES5的代码(可能还有bug没有发现)。但是在当前的实现中,所有的运行结果都是放在一个类似沙盒的地方,无法对外界产生影响。如果要把运行结果取出来,可能的办法有两种。第一种是传入一个全局的变量,把影响作用在这个全局变量当中,借助它把结果带出来;另外一种则是让解析器支持export语法,能够把export语句声明的结果返回,感兴趣的读者可以自行研究。最后,这个JS解析器已经在我的Github上开源,欢迎前来交流~https://github.com/jrainlau/c…参考资料从零开始写一个Javascript解析器微信小程序也要强行热更代码,鹅厂不服你来肛我呀jkeylu/evil-eval ...

December 3, 2018 · 6 min · jiezi

精读《手写 SQL 编译器 - 性能优化之缓存》

1 引言重回 “手写 SQL 编辑器” 系列。这次介绍如何利用缓存优化编译器执行性能。可以利用 Frist 集 与 Match 节点缓存 这两种方式优化。本文会用到一些图做解释,下面介绍图形规则:First 集优化,是指在初始化时,将整体文法的 First 集找到,因此在节点匹配时,如果 Token 不存在于 First 集中,可以快速跳过这个文法,在文法调用链很长,或者 “或” 的情况比较多时,可以少走一些弯路:如图所示,只要构建好了 First 集,不论这个节点的路径有多长,都可以以最快速度判断节点是否不匹配。如果节点匹配,则继续深度遍历方式访问节点。现在节点不匹配时性能已经最优,那下一步就是如何优化匹配时的性能,这时就用到 Match 节点缓存。Match 节点缓存,指在运行时,缓存节点到其第一个终结符的过程。与 First 集相反,First 集可以快速跳过,而 Match 节点缓存可以快速找到终结符进行匹配,在非终结符很多时,效果比较好:如图所示,当匹配到节点时,如果已经构建好了缓存,可以直接调到真正匹配 Token 的 Match 节点,从而节省了大量节点遍历时间。这里需要注意的是,由于 Tree 节点存在分支可能性,因此缓存也包含将 “沿途” Chances 推入 Chances 池的职责。2 精读那么如何构建 First 集与 Match 节点缓存呢?通过两张图解释。构建 First 集如图所示,构建 First 集是个自下而上的过程,当访问到 MatchNode 节点时,就可以收集作为父节点的 First 集了!父集判断 First 集收集完毕的话,就会触发它的父节点 First 集收集判断,如此递归,最后完成 First 集收集的是最顶级节点。构建 Match 节点缓存如图所示,访问节点时,如果没有缓存,则会将这个节点添加到 Match 缓存查找队列,同时路途遇到 TreeNode,也会将下一个 Chance 添加到缓存查找队列。直到遇到了第一个 MatchNode 节点,则这个节点是 “Match 缓存查找队列” 所有节点的 Match 节点缓存,此时这些节点的缓存就可以生效了,指向这个 MatchNode,同时清空缓存查找队列,等待下一次查找。3 总结拿 select a, b, c, d from e 这个语句做测试:node 节点访问次数Frist 集优化First 集 + Match 节点缓存优化784669652从这个简单 Demo 来看,提效了 16% 左右。不过考虑到文法结构会影响到提效,对于层级更深的文法、能激活深层级文法的输入可以达到更好的效率提升。4 更多讨论讨论地址是:精读《手写 SQL 编译器 - 性能优化之缓存》 · Issue #110 · dt-fe/weekly如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。 ...

November 5, 2018 · 1 min · jiezi