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

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

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

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

  第一章:

package interpreter

import (
"fmt"
"unicode"
) // Token types
//
// EOF (end-of-file) token is used to indicate that
// there is no more input left for lexical analysis
type TokenType int const (
cTokenTypeOfNone TokenType = iota
cTokenTypeOfInteger
cTokenTypeOfPlusSign
cTokenTypeOfEOF
) type token struct {
t TokenType // token type: INTEGER, PLUS, or EOF
v interface{} // token value: 0, 1, 2. 3, 4, 5, 6, 7, 8, 9, '+', or None
} func newToken(t TokenType, v interface{}) token {
return token{
t: t,
v: v,
}
} type Interpreter struct {
text []rune // client string input, e.g. "3+5"
pos int // an index into text
currToken token // current token instance
} func New() *Interpreter {
return &Interpreter{
text: []rune(""),
pos: 0,
currToken: newToken(cTokenTypeOfNone, nil),
}
} func convToDigit(c rune) (int, bool) {
if unicode.IsDigit(c) {
return int(c - '0'), true
}
return 0, false
} // Lexical analyzer (also known as scanner or tokenizer)
//
// This method is responsible for breaking a sentence apart into tokens.
// One token at a time.
func (self *Interpreter) getNextToken() token {
text := self.text // is self.pos index past the end of the self.text ?
// if so, then return EOF token because there is no more
// input left to convert into tokens
if self.pos > len(text)-1 {
return newToken(cTokenTypeOfEOF, nil)
} // get a character at the position self.pos and decide
// what token to create based on the single character
// var currChar interface{} = text[self.pos]
currChar := text[self.pos] // if the character is a digit then convert it to
// integer, create an INTEGER token, increment self.pos
// index to point to the next character after the digit,
// and return the INTEGER token
if v, ok := convToDigit(text[self.pos]); ok {
self.pos += 1
return newToken(cTokenTypeOfInteger, v)
} if currChar == '+' {
self.pos += 1
return newToken(cTokenTypeOfPlusSign, '+')
} panic(fmt.Sprintf("Error parsing input: %s", string(self.text)))
} // compare the current token type with the passed token type
// and if they match then "eat" the current token
// and assign the next token to the self.currToken,
// otherwise raise an exception.
func (self *Interpreter) eat(tokenType TokenType) {
if self.currToken.t == tokenType {
self.currToken = self.getNextToken()
return
} panic(fmt.Sprintf("Error parsing input: %s", self.text))
} // parse "INTEGER PLUS INTEGER"
func (self *Interpreter) Parse(s string) int {
self.text = []rune(s)
self.pos = 0 // set current token to the first token taken from the input
self.currToken = self.getNextToken() // we expect the current token to be a single-digit integer
left := self.currToken
self.eat(cTokenTypeOfInteger) // we expect the current token to be a '+' token
// op := self.currToken
self.eat(cTokenTypeOfPlusSign) // we expect the current token to be a single-digit integer
right := self.currToken
self.eat(cTokenTypeOfInteger) // after the above call the self.current_token is set to EOF token.
// at this point INTEGER PLUS INTEGER sequence of tokens
// has been successfully found and the method can just
// return the result of adding two integers, thus
// effectively interpreting client input
return left.v.(int) + right.v.(int)
}

  第二章:

package interpreter

import (
"fmt"
"unicode" "github.com/ecofast/rtl/sysutils"
) // Token types
//
// EOF (end-of-file) token is used to indicate that
// there is no more input left for lexical analysis
type TokenType int const (
cTokenTypeOfNone TokenType = iota
cTokenTypeOfInteger
cTokenTypeOfPlusSign
cTokenTypeOfMinusSign
cTokenTypeOfEOF
) type token struct {
t TokenType // token type: INTEGER, PLUS, MINUS, or EOF
v interface{} // token value: non-negative integer value, '+', '-', or None
} func newToken(t TokenType, v interface{}) token {
return token{
t: t,
v: v,
}
} type Interpreter struct {
text []rune // client string input, e.g. "3 + 5", "12 - 5", etc
pos int // an index into text
currToken token // current token instance
currChar rune
} func New() *Interpreter {
return &Interpreter{
text: []rune(""),
pos: 0,
currToken: newToken(cTokenTypeOfNone, nil),
currChar: 0,
}
} // Advance the 'pos' pointer and set the 'currChar' variable
func (self *Interpreter) advance() {
self.pos += 1
if self.pos > len(self.text)-1 {
self.currChar = 0
} else {
self.currChar = self.text[self.pos]
}
} func (self *Interpreter) skipWhiteSpace() {
for self.currChar != 0 && unicode.IsSpace(self.currChar) {
self.advance()
}
} // Return a (multidigit) integer consumed from the input
func (self *Interpreter) integer() int {
ret := ""
for self.currChar != 0 && unicode.IsDigit(self.currChar) {
ret += string(self.currChar)
self.advance()
}
return sysutils.StrToInt(ret)
} // Lexical analyzer (also known as scanner or tokenizer)
//
// This method is responsible for breaking a sentence apart into tokens.
func (self *Interpreter) getNextToken() token {
for self.currChar != 0 {
if unicode.IsSpace(self.currChar) {
self.skipWhiteSpace()
continue
} if unicode.IsDigit(self.currChar) {
return newToken(cTokenTypeOfInteger, self.integer())
} if self.currChar == '+' {
self.advance()
return newToken(cTokenTypeOfPlusSign, '+')
} if self.currChar == '-' {
self.advance()
return newToken(cTokenTypeOfMinusSign, '-')
} panic(fmt.Sprintf("Error parsing input: %s", string(self.text)))
}
return newToken(cTokenTypeOfEOF, nil)
} // compare the current token type with the passed token type
// and if they match then "eat" the current token
// and assign the next token to the self.currToken,
// otherwise raise an exception.
func (self *Interpreter) eat(tokenType TokenType) {
if self.currToken.t == tokenType {
self.currToken = self.getNextToken()
return
} panic(fmt.Sprintf("Error parsing input: %s", self.text))
} // parse "INTEGER PLUS INTEGER" or "INTEGER MINUS INTEGER"
func (self *Interpreter) Parse(s string) int {
self.text = []rune(s)
self.pos = 0
self.currChar = self.text[self.pos] // set current token to the first token taken from the input
self.currToken = self.getNextToken() // we expect the current token to be an integer
left := self.currToken
self.eat(cTokenTypeOfInteger) // we expect the current token to be either a '+' or '-'
op := self.currToken
if op.t == cTokenTypeOfPlusSign {
self.eat(cTokenTypeOfPlusSign)
} else {
self.eat(cTokenTypeOfMinusSign)
} // we expect the current token to be an integer
right := self.currToken
self.eat(cTokenTypeOfInteger) // after the above call the self.current_token is set to EOF token.
// at this point either the INTEGER PLUS INTEGER or
// the INTEGER MINUS INTEGER sequence of tokens
// has been successfully found and the method can just
// return the result of adding or subtracting two integers, thus
// effectively interpreting client input
if op.t == cTokenTypeOfPlusSign {
return left.v.(int) + right.v.(int)
}
return left.v.(int) - right.v.(int)
}

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

// part2 project main.go
package main import (
"bufio"
"fmt"
"os"
"part2/interpreter"
"strings"
) func main() {
fmt.Println("Let's Build A Simple Interpreter - Part 2") parser := interpreter.New()
reader := bufio.NewReader(os.Stdin)
for {
if s, err := reader.ReadString('\n'); err == nil {
fmt.Println(parser.Parse(strings.TrimSpace(s)))
continue
}
break
}
}

  

  本兴趣项目已托管至 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. 【JavaScript框架封装】实现一个类似于JQuery的事件框架的封装

    // 事件框架 (function (xframe) { // 需要参与链式访问的(必须使用prototype的方式来给对象扩充方法) xframe.extend({ /** * 实现一个浏览器的基本 ...

  2. javascript 富文本 注意事项

    富文本编辑器 div内嵌iframe iframe body contenteditable属性 true 整个iframe 即为可编辑框,创建时注意事项: 1.编辑 焦点问题 弹出新控件时为控件设置 ...

  3. 使用shell脚本定时备份web网站代码

    #!/bin/bash ############### common file ################ #备份文件存放目录 WEBBACK_DIR="/data/backup/ba ...

  4. Mysql错误:#1054 - Unknown column '字段名' in 'field list'

    # 1054 - Unknown column '字段名' in 'field list' 第一个就是你的表中没有这个字段 另一个就是你的这个字段前后可能有空格!!!,去掉空格即可!

  5. 马上着手开发ios应用程序

    https://developer.apple.com/library/ios/referencelibrary/GettingStarted/RoadMapiOSCh/chapters/Introd ...

  6. mysql 插入更新在一条sql ON DUPLICATE KEY UPDATE

    有时候需要进行数据操作的,如果有数据则更新数据, 没有数据则插入. 以往的做法是先查询,再根据查询结果进行判断,执行插入或更新操作 其实 有一种 ON DUPLICATE KEY UPDATE 语法, ...

  7. Mysql 索引-1

    索引的类型 根据数据库的功能,可以在数据库设计器中创建四种索引:唯一索引.非唯一索引.主键索引和聚集索引. 索引的不同应用场景 场景 1. 当数据多且字段值有相同的值得时候用普通索引. 2. 当字段多 ...

  8. socketpair和pipe的区别

    http://blog.csdn.net/bingqingsuimeng/article/details/9055499 管道pipe是半双工的,pipe两次才能实现全双工,使得代码复杂.socket ...

  9. POJ 1320

    作弊了--!该题可以通过因式分解得到一个佩尔方程....要不是学着这章,估计想不到.. 得到x1,y1后,就直接代入递推式递推了 x[n]=x[n-1]*x[1]+d*y[n-1]*y[1] y[n] ...

  10. cocos2d-x 3.0 经常使用对象的创建方式

    cocos2d-x 3.0 中全部对象差点儿都能够用create函数来创建,其它的创建方式也是有create函数衍生. 以下来介绍下create函数创建一般对象的方法,省得开发中常常忘记啥的. 1.精 ...