一直以来对编译器/解释器等都较有兴趣。我非科班出身,当初还在大学时,只是马马虎虎看完了《编译原理》之类教材,上机非常少,对龙书之类圣经也只是浅尝辄止而已。工作至今,基本已将编译原理相关知识忘记得差不多了,可能也就还对譬如预处理词法分析语法分析 AST 生成等基础性的概念还有点印象罢。

  约 1 年多前,我也有想法搞一套基于简化的 Pascal 语法的带类型的脚本语言“编译器”(PaxCompiler 之类可能太复杂了),并将此脚本语言编写的脚本与 Golang 交互起来。当然这只是我个人的业余兴趣而已,至于是否会付诸行动、能搞成怎样都是未知。而选择 Pascal 作为参考改编语言的原因,其一我比较喜欢它的语言设计,其二它曾是我某段时间内的工作语言所以感情成分使然,其三较之诸如 Python、Lua 我更喜欢带类型的脚本语言(TypeScript?我不太喜欢 JavaScript 的语法...),当然,Pascal 的语法形式也确实比较方便为之开发编译器/解释器。

  而短期内,个人恐怕没有太多精力去啃龙书之类,于是索性,看点基础资料且按此系列教程之类慢慢温习并从 tokenizer 开始一步步实现自己的 EcoPascal——即便最终,它只是个玩具脚本语言而已。

  近 2 天趁有空,粗略看了前文所述教程的前两章,并以 Golang 重写了这两章里的解释程序(代码写得有些粗放)。

  第一章:

  1. package interpreter
  2.  
  3. import (
  4. "fmt"
  5. "unicode"
  6. )
  7.  
  8. // Token types
  9. //
  10. // EOF (end-of-file) token is used to indicate that
  11. // there is no more input left for lexical analysis
  12. type TokenType int
  13.  
  14. const (
  15. cTokenTypeOfNone TokenType = iota
  16. cTokenTypeOfInteger
  17. cTokenTypeOfPlusSign
  18. cTokenTypeOfEOF
  19. )
  20.  
  21. type token struct {
  22. t TokenType // token type: INTEGER, PLUS, or EOF
  23. v interface{} // token value: 0, 1, 2. 3, 4, 5, 6, 7, 8, 9, '+', or None
  24. }
  25.  
  26. func newToken(t TokenType, v interface{}) token {
  27. return token{
  28. t: t,
  29. v: v,
  30. }
  31. }
  32.  
  33. type Interpreter struct {
  34. text []rune // client string input, e.g. "3+5"
  35. pos int // an index into text
  36. currToken token // current token instance
  37. }
  38.  
  39. func New() *Interpreter {
  40. return &Interpreter{
  41. text: []rune(""),
  42. pos: 0,
  43. currToken: newToken(cTokenTypeOfNone, nil),
  44. }
  45. }
  46.  
  47. func convToDigit(c rune) (int, bool) {
  48. if unicode.IsDigit(c) {
  49. return int(c - '0'), true
  50. }
  51. return 0, false
  52. }
  53.  
  54. // Lexical analyzer (also known as scanner or tokenizer)
  55. //
  56. // This method is responsible for breaking a sentence apart into tokens.
  57. // One token at a time.
  58. func (self *Interpreter) getNextToken() token {
  59. text := self.text
  60.  
  61. // is self.pos index past the end of the self.text ?
  62. // if so, then return EOF token because there is no more
  63. // input left to convert into tokens
  64. if self.pos > len(text)-1 {
  65. return newToken(cTokenTypeOfEOF, nil)
  66. }
  67.  
  68. // get a character at the position self.pos and decide
  69. // what token to create based on the single character
  70. // var currChar interface{} = text[self.pos]
  71. currChar := text[self.pos]
  72.  
  73. // if the character is a digit then convert it to
  74. // integer, create an INTEGER token, increment self.pos
  75. // index to point to the next character after the digit,
  76. // and return the INTEGER token
  77. if v, ok := convToDigit(text[self.pos]); ok {
  78. self.pos += 1
  79. return newToken(cTokenTypeOfInteger, v)
  80. }
  81.  
  82. if currChar == '+' {
  83. self.pos += 1
  84. return newToken(cTokenTypeOfPlusSign, '+')
  85. }
  86.  
  87. panic(fmt.Sprintf("Error parsing input: %s", string(self.text)))
  88. }
  89.  
  90. // compare the current token type with the passed token type
  91. // and if they match then "eat" the current token
  92. // and assign the next token to the self.currToken,
  93. // otherwise raise an exception.
  94. func (self *Interpreter) eat(tokenType TokenType) {
  95. if self.currToken.t == tokenType {
  96. self.currToken = self.getNextToken()
  97. return
  98. }
  99.  
  100. panic(fmt.Sprintf("Error parsing input: %s", self.text))
  101. }
  102.  
  103. // parse "INTEGER PLUS INTEGER"
  104. func (self *Interpreter) Parse(s string) int {
  105. self.text = []rune(s)
  106. self.pos = 0
  107.  
  108. // set current token to the first token taken from the input
  109. self.currToken = self.getNextToken()
  110.  
  111. // we expect the current token to be a single-digit integer
  112. left := self.currToken
  113. self.eat(cTokenTypeOfInteger)
  114.  
  115. // we expect the current token to be a '+' token
  116. // op := self.currToken
  117. self.eat(cTokenTypeOfPlusSign)
  118.  
  119. // we expect the current token to be a single-digit integer
  120. right := self.currToken
  121. self.eat(cTokenTypeOfInteger)
  122.  
  123. // after the above call the self.current_token is set to EOF token.
  124. // at this point INTEGER PLUS INTEGER sequence of tokens
  125. // has been successfully found and the method can just
  126. // return the result of adding two integers, thus
  127. // effectively interpreting client input
  128. return left.v.(int) + right.v.(int)
  129. }

  第二章:

  1. package interpreter
  2.  
  3. import (
  4. "fmt"
  5. "unicode"
  6.  
  7. "github.com/ecofast/rtl/sysutils"
  8. )
  9.  
  10. // Token types
  11. //
  12. // EOF (end-of-file) token is used to indicate that
  13. // there is no more input left for lexical analysis
  14. type TokenType int
  15.  
  16. const (
  17. cTokenTypeOfNone TokenType = iota
  18. cTokenTypeOfInteger
  19. cTokenTypeOfPlusSign
  20. cTokenTypeOfMinusSign
  21. cTokenTypeOfEOF
  22. )
  23.  
  24. type token struct {
  25. t TokenType // token type: INTEGER, PLUS, MINUS, or EOF
  26. v interface{} // token value: non-negative integer value, '+', '-', or None
  27. }
  28.  
  29. func newToken(t TokenType, v interface{}) token {
  30. return token{
  31. t: t,
  32. v: v,
  33. }
  34. }
  35.  
  36. type Interpreter struct {
  37. text []rune // client string input, e.g. "3 + 5", "12 - 5", etc
  38. pos int // an index into text
  39. currToken token // current token instance
  40. currChar rune
  41. }
  42.  
  43. func New() *Interpreter {
  44. return &Interpreter{
  45. text: []rune(""),
  46. pos: 0,
  47. currToken: newToken(cTokenTypeOfNone, nil),
  48. currChar: 0,
  49. }
  50. }
  51.  
  52. // Advance the 'pos' pointer and set the 'currChar' variable
  53. func (self *Interpreter) advance() {
  54. self.pos += 1
  55. if self.pos > len(self.text)-1 {
  56. self.currChar = 0
  57. } else {
  58. self.currChar = self.text[self.pos]
  59. }
  60. }
  61.  
  62. func (self *Interpreter) skipWhiteSpace() {
  63. for self.currChar != 0 && unicode.IsSpace(self.currChar) {
  64. self.advance()
  65. }
  66. }
  67.  
  68. // Return a (multidigit) integer consumed from the input
  69. func (self *Interpreter) integer() int {
  70. ret := ""
  71. for self.currChar != 0 && unicode.IsDigit(self.currChar) {
  72. ret += string(self.currChar)
  73. self.advance()
  74. }
  75. return sysutils.StrToInt(ret)
  76. }
  77.  
  78. // Lexical analyzer (also known as scanner or tokenizer)
  79. //
  80. // This method is responsible for breaking a sentence apart into tokens.
  81. func (self *Interpreter) getNextToken() token {
  82. for self.currChar != 0 {
  83. if unicode.IsSpace(self.currChar) {
  84. self.skipWhiteSpace()
  85. continue
  86. }
  87.  
  88. if unicode.IsDigit(self.currChar) {
  89. return newToken(cTokenTypeOfInteger, self.integer())
  90. }
  91.  
  92. if self.currChar == '+' {
  93. self.advance()
  94. return newToken(cTokenTypeOfPlusSign, '+')
  95. }
  96.  
  97. if self.currChar == '-' {
  98. self.advance()
  99. return newToken(cTokenTypeOfMinusSign, '-')
  100. }
  101.  
  102. panic(fmt.Sprintf("Error parsing input: %s", string(self.text)))
  103. }
  104. return newToken(cTokenTypeOfEOF, nil)
  105. }
  106.  
  107. // compare the current token type with the passed token type
  108. // and if they match then "eat" the current token
  109. // and assign the next token to the self.currToken,
  110. // otherwise raise an exception.
  111. func (self *Interpreter) eat(tokenType TokenType) {
  112. if self.currToken.t == tokenType {
  113. self.currToken = self.getNextToken()
  114. return
  115. }
  116.  
  117. panic(fmt.Sprintf("Error parsing input: %s", self.text))
  118. }
  119.  
  120. // parse "INTEGER PLUS INTEGER" or "INTEGER MINUS INTEGER"
  121. func (self *Interpreter) Parse(s string) int {
  122. self.text = []rune(s)
  123. self.pos = 0
  124. self.currChar = self.text[self.pos]
  125.  
  126. // set current token to the first token taken from the input
  127. self.currToken = self.getNextToken()
  128.  
  129. // we expect the current token to be an integer
  130. left := self.currToken
  131. self.eat(cTokenTypeOfInteger)
  132.  
  133. // we expect the current token to be either a '+' or '-'
  134. op := self.currToken
  135. if op.t == cTokenTypeOfPlusSign {
  136. self.eat(cTokenTypeOfPlusSign)
  137. } else {
  138. self.eat(cTokenTypeOfMinusSign)
  139. }
  140.  
  141. // we expect the current token to be an integer
  142. right := self.currToken
  143. self.eat(cTokenTypeOfInteger)
  144.  
  145. // after the above call the self.current_token is set to EOF token.
  146. // at this point either the INTEGER PLUS INTEGER or
  147. // the INTEGER MINUS INTEGER sequence of tokens
  148. // has been successfully found and the method can just
  149. // return the result of adding or subtracting two integers, thus
  150. // effectively interpreting client input
  151. if op.t == cTokenTypeOfPlusSign {
  152. return left.v.(int) + right.v.(int)
  153. }
  154. return left.v.(int) - right.v.(int)
  155. }

  有了“核心”解释程序,使用起来就很简单了:

  1. // part2 project main.go
  2. package main
  3.  
  4. import (
  5. "bufio"
  6. "fmt"
  7. "os"
  8. "part2/interpreter"
  9. "strings"
  10. )
  11.  
  12. func main() {
  13. fmt.Println("Let's Build A Simple Interpreter - Part 2")
  14.  
  15. parser := interpreter.New()
  16. reader := bufio.NewReader(os.Stdin)
  17. for {
  18. if s, err := reader.ReadString('\n'); err == nil {
  19. fmt.Println(parser.Parse(strings.TrimSpace(s)))
  20. continue
  21. }
  22. break
  23. }
  24. }

  

  本兴趣项目已托管至 Github,比较可能会不定期慢慢更新。

《Let's Build A Simple Interpreter》之 Golang 版的更多相关文章

  1. 简单物联网:外网访问内网路由器下树莓派Flask服务器

    最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...

  2. 利用ssh反向代理以及autossh实现从外网连接内网服务器

    前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...

  3. 外网访问内网Docker容器

    外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...

  4. 外网访问内网SpringBoot

    外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...

  5. 外网访问内网Elasticsearch WEB

    外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...

  6. 怎样从外网访问内网Rails

    外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...

  7. 怎样从外网访问内网Memcached数据库

    外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...

  8. 怎样从外网访问内网CouchDB数据库

    外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...

  9. 怎样从外网访问内网DB2数据库

    外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...

  10. 怎样从外网访问内网OpenLDAP数据库

    外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...

随机推荐

  1. CSS - 内联元素span 强制换行失败的可能原因

    在CSS中,标签span 强制换行失败:(使用display:block) 可能原因:float:left   or  float:right

  2. 区分JAVA创建线程的几种方法

    1. start()和run()       通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行.然后 通过此Thread类调用方法run()来完成其运行操 ...

  3. nyoj27-水池数目【DFS】

    题目描述: 南阳理工学院校园里有一些小河和一些湖泊,现在,我们把它们通一看成水池,假设有一张我们学校的某处的地图,这个地图上仅标识了此处是否是水池,现在,你的任务来了,请用计算机算出该地图中共有几个水 ...

  4. [CodeForces]986A Fair

    大意:给一张图,每个图上有一个数,问以每个点为源点,经过的点包含k种数字的最小距离. 显然跑最短路会T,但我们注意到边权一定.某次学校考试就是类似题,可以bfs做,复杂度O(n),每种货物做一次,复杂 ...

  5. CF914A Perfect Squares

    CF914A Perfect Squares 题意翻译 给定一组有n个整数的数组a1,a2,…,an.找出这组数中的最大非完全平方数. 完全平方数是指有这样的一个数x,存在整数y,使得x=y^2y2  ...

  6. POJ 2111

    记忆化搜索即可,设DP[I][J]为可到达的最大步数. 输出时用了一种较笨拙的方法,还有一种方法是使用最长上升子序列的方式,挺好,先排序,这让我想起上次BESTCODER的一题 #include &l ...

  7. POJ 2190

    直接枚举0~X就可以了...我开始竟然往扩展欧几里德定理想了,呃呃--- #include <iostream> #include <cstdlib> #include < ...

  8. 通达OA 小飞鱼工作流在线培训教程(一)HTML基础介绍

    应一些刚接触工作流设计朋友的要求,这里开设一个系列教程,对通达OA工作流设计相关的内容做个介绍.方便解决一些日常经常出现的问题,希望对刚刚接触这部分工作的朋友能够有些帮助. 工作流设计须要多方面的知识 ...

  9. HTML5开发移动web应用——Sencha Touch篇(8)

    DataView是Sencha Touch中最重要的组件,用于数据的可视化.数据可视化的重要性不言而喻,能够讲不论什么数据以形象的方式展示给用户. 眼下,怎样更好地可视化是很多公司或框架都在追求的. ...

  10. Autodesk 举办的 Revit 2015 二次开发速成( 1.5 天),教室培训, 地点武汉

    2014年8月26日9:00 – 17:00 2014年8月27日9:00 – 12:00 培训地点: Ø 湖北工业大学 实训楼605教室 Ø 地址:武汉市武昌区南湖李家墩一村一号 Ø 交通路线说明: ...