一直以来对编译器/解释器等都较有兴趣。我非科班出身,当初还在大学时,只是马马虎虎看完了《编译原理》之类教材,上机非常少,对龙书之类圣经也只是浅尝辄止而已。工作至今,基本已将编译原理相关知识忘记得差不多了,可能也就还对譬如预处理词法分析语法分析 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. springMVC3.0(文件上传,@RequestMapping加參数,@SessionAttributes,@ModelAttribute,转发,重定向,数值获取,传參,ajax,拦截器)

    1.项目包结构例如以下: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdG90b3R1enVvcXVhbg==/font/5a6L5L2T/fontsiz ...

  3. HDU 1269 -- 迷宫城堡【有向图求SCC的数目 && 模板】

    迷宫城堡 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submi ...

  4. java 基本数据类型及自己主动类型提升

    基本数据类型:8种 1.整型: byte    1个字节    8位    -128到127 short   2个字节    16位  -2^15到(2^15)-1 int    4个字节    32 ...

  5. php面向对象之构造函数和析构函数

    php面向对象之构造函数和析构函数 简介 php面向对象支持两种形式的构造函数和析构函数,一种是和类同名的构造函数(php5.5之前),一类是魔术方法(php5.5之后).与类名相同的构造函数优先级比 ...

  6. nyoj--120--校园网络(scc+缩点)

    校园网络 时间限制:3000 ms  |  内存限制:65535 KB 难度:5 描述 南阳理工学院共有M个系,分别编号1~M,其中各个系之间达成有一定的协议,如果某系有新软件可用时,该系将允许一些其 ...

  7. hdoj--2122--Ice_cream’s world III(克鲁斯卡尔)

    Ice_cream's world III Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Ot ...

  8. Tool-DB:Navicat

    ylbtech-Tool-DB:Navicat Navicat是一套快速.可靠并价格相当便宜的数据库管理工具,专为简化数据库的管理及降低系统管理成本而设.它的设计符合数据库管理员.开发人员及中小企业的 ...

  9. 在Maven中引入spring的DAO、DOMAIN、CONTROLLER、VIEW

    除了mysql外麻雀虽小,五脏俱全. 参照之前的博客建立好的maven项目如图. 第一步 : 向maven项目中的pom文件添加依赖 ,然后maven install

  10. git服务器搭建-gitosis

    需求 硬件需求:一台Ubuntu或者debian电脑(虚拟机),能通过网络访问到. 软件需求:git-core, gitosis, openssh-server, openssh-client, Ap ...