在编译器开发中有两个非常重要的工具名为lex和yacc,他们是编译器的生成器。本质上我们不需要一行行去完成编译器的代码,只需要借助这两个工具,同时制定好词法解析和语法解析的规则后,这两个工具就会自动帮我们把代码生成,我们后续的任务就是使用go语言将这两个工具实现。
为了更好的理解我们要开发的GoLex,我们先熟悉一下lex工具的使用。在Centos上安装lex的命令为:
代码语言:javascript复制yum install flex
接下来我们看看它的使用。lex的作用主要是根据给定正则表达式,然后lex会把既定的正则表达式生成成对应的C语言代码,我们将生成的代码编译后就能得到可以针对输入进行相应识别的程序,我们看看一个具体例子。安装完flex后,我们在本地创建一个名为ch1-02.l的文件,然后输入内容如下:
代码语言:javascript复制%{
/*
这里是一段直接拷贝到c文件的代码
*/
%}
alpha [a-zA-Z]
%%
[t] /*吸收所有空格*/
is |
am |
are |
where |
was |
be |
being |
been |
does |
did |
will |
would |
should |
can |
could |
has |
have |
had |
go {printf("%s: is a verbn", yytext);}
{alpha} {printf("%s: is not a verbn", yytext);}
.|n {ECHO; /*normal default anywat*/}
%%
int yywrap(){return 1;}
main() {
yylex(); //启动识别过程
}
文件格式分为三部分,第一部分位于{% %}之间,这里面用于放置我们自己写的C语言代码,通常情况下是一些变量定义。在%}和%%之间存放正则表达式的宏定义,例如“alpha [a-zA-Z]”。在%% %%之间则对应用于识别字符串的正则表达式,这里需要注意的是,在表达式后面还有一段C语言代码,他们会在字符串匹配上之后执行,因此这些代码也称为”Action”。
在第二个%%之后是我们编写的C语言代码,这里面我们定义了yywrap函数,这个函数是lex要求的回调,它的作用我们以后再考虑,在提交的main函数中,我们调用yylex函数启动对输入数据的识别过程,这个函数正是lex通过识别正则表达式,构造状态机,然后将后者转换为可执行代码后对应的函数。
有了上面文件后,我们执行如下命令:
代码语言:javascript复制lex ch1-02.l
完成后就会在本地生成一个名为lex.yy.c的文件,里面的代码非常复杂,同时这些代码完全由lex生成。最后我们用如下命令编译:
代码语言:javascript复制cc lex.yy.c
然后会在本地生成可执行文件a.out,执行a.out后程序运行起来,然后我们就可以输入相应字符串,如果对应字符串满足给定正则表达式,例如输入字符串中包含”should”, “would”等单词时,对应的语句printf(“%s: is a verbn”, yytext);就可以执行,情况如下图所示:
总结来说lex和yacc就是开发编译器的框架,他们负责识别和解析,然后把结果传递给我们提供的函数。接下来我们开发的GoLex就是我们当前介绍lex程序的go语言实现,同时我尝试把其中的C语言转换成python语言。
首先我们创建GoLex工程,然后在其中创建nfa文件夹,然后执行如下命令初始化:
代码语言:javascript复制go mod init nfa
go mod tidy
首先我们看看对应的.l文件,创建input.l,输入内容如下:
代码语言:javascript复制%{
FCON = 1
ICON = 2
%}
D [0-9]
%%
(e{D} )?
%%
我们将读取上面内容并根据给定的正则表达式创建NFA状态机。我们先在里面创建一些支持类功能的代码,第一个事debugger.go文件,它用来输出函数的调用信息,其内容如下:
代码语言:javascript复制package nfa
import (
"fmt"
"strings"
)
type Debugger struct {
level int
}
var DEBUG *Debugger
func newDebugger() *Debugger {
return &Debugger{
level: 0,
}
}
func DebuggerInstance() *Debugger {
if DEBUG == nil {
DEBUG = newDebugger()
}
return DEBUG
}
func (d *Debugger) Enter(name string) {
s := strings.Repeat("*", d.level*4) "entering: " name
fmt.Println(s)
d.level = 1
}
func (d *Debugger) Leave(name string) {
d.level -= 1
s := strings.Repeat("*", d.level*4) "leaving: " name
fmt.Println(s)
}
debugger的功能就是显示出函数嵌套调用的次序,前面我们实现的编译器语法解析部分,函数会层级调用,因此有效显示出调用信息会帮助我们更好的查找实现逻辑中的Bug,它展示的信息在我们上一节展示过,当函数嵌套时,被调用函数的输出相对于符函数,它会向右挪到四个字符的位置。
接下来我们实现错误输出功能,创建文件parse_error.go,其内容如下:
代码语言:javascript复制package nfa
type ERROR_TYPE int
const (
E_BADREXPR ERROR_TYPE = iota //表达式字符串有错误
E_PAREN //少了右括号
E_LENGTH //正则表达式数量过多
E_BRACKET //字符集没有以[开始
E_BOL // ^ 必须出现在表达式字符串的起始位置
E_CLOSE //*, , ? 等操作符前面没有表达式
E_STRINGS //action 代码字符串过长
E_NEWLINE //在双引号包含的字符串中出现回车换行
E_BADMAC //表达式中的宏定义少了右括号}
E_NOMAC //宏定义不存在
E_MACDEPTH //宏定义嵌套太深
)
type ParseError struct {
err_msgs []string
}
func NewParseError() *ParseError {
return &ParseError{
err_msgs: []string{
"MalFormed regular expression",
"Missing close parenthesis",
"Too many regular expressions or expression too long",
"Missing [ in character class",
"^ must be at start of expression",
"Newline in quoted string, use \n instead",
"Missing } in macro expansion",
"Macro doesn't exist",
"Macro expansions nested too deeply",
},
}
}
func (p *ParseError) ParseErr(errType ERROR_TYPE) {
panic(p.err_msgs[int(errType)])
}
上面代码的目的是,在解析过程中发现错误时,我们打印对应的错误信息。接下来我们需要做的是识别输入文件中的宏定义,因此创建文件macro.go,里面实现代码如下:
代码语言:javascript复制package nfa
import (
"errors"
"fmt"
"strings"
)
type Macro struct {
//例如 "D [0-9]" 那么D就是宏定义的名称,[0-9]就是内容
Name string
Text string
}
type MacroManager struct {
macroMap map[string]*Macro
}
var macroManagerInstance *MacroManager
func GetMacroManagerInstance() *MacroManager {
if macroManagerInstance == nil {
macroManagerInstance = newMacroManager()
}
return macroManagerInstance
}
func newMacroManager() *MacroManager {
return &MacroManager{
macroMap: make(map[string]*Macro),
}
}
func (m *MacroManager) PrintMacs() {
for _, val := range m.macroMap {
fmt.Sprintf("mac name: %s, text %s: ", val.Name, val.Text)
}
}
func (m *MacroManager) NewMacro(line string) (*Macro, error) {
//输入对应宏定义的一行内容例如 D [0-9]
line = strings.TrimSpace(line)
nameAndText := strings.Fields(line)
if len(nameAndText) != 2 {
return nil, errors.New("macro string error ")
}
/*
如果宏定义出现重复,那么后面的定义就直接覆盖前面
例如 :
D [0-9]
D [a-z]
那么我们采用最后一个定义也就是D被扩展成[a-z]
*/
macro := &Macro{
Name: nameAndText[0],
Text: nameAndText[1],
}
m.macroMap[macro.Name] = macro
return macro, nil
}
func (m *MacroManager) ExpandMacro(macroStr string) string {
/*
输入: D}, 然后该函数将其转换为[0-9]
左括号会被调用函数去除
*/
valid := false
macroName := ""
for pos, char := range macroStr {
if char == '}' {
valid = true
macroName = macroStr[0:pos]
break
}
}
if valid != true {
NewParseError().ParseErr(E_BADREXPR)
}
macro, ok := m.macroMap[macroName]
if !ok {
NewParseError().ParseErr(E_NOMAC)
}
return macro.Text
}
上面代码实现的目的是识别宏定义,将其存储在一个map中,后面在解析正则表达式时,一旦遇到宏定义,例如我们定义了宏定义:
代码语言:javascript复制D [0-9]
然后在后续表达式中遇到宏定义时,例如:
代码语言:javascript复制(e{D} )?
当读取到{D}时,我们会将其替换成[0-9],这个替换功能正是由ExpandMacro函数来实现。下面我们先定义状态机节点对应的数据结构,创建nfa.go,实现代码如下:
代码语言:javascript复制package nfa
type EdgeType int
const (
EPSILON EdgeType = iota //epsilon 边
CCL //边对应输入是字符集
)
type Anchor int
const (
NONE Anchor = iota
START //表达式开头包含符号^
END //表达式末尾包含$
BOTH //开头包含^同时末尾包含$
)
var NODE_STATE int = 0
type NFA struct {
edge EdgeType
bitset map[string]bool //边对应的输入是字符集例如[A-Z]
state int
next *NFA //一个nfa节点最多有两条边
next2 *NFA
accept string //当进入接收状态后要执行的代码
anchor Anchor //表达式是否在开头包含^或是在结尾包含$
}
func NewNFA() *NFA {
node := &NFA{
edge: EPSILON,
bitset: make(map[string]bool),
next: nil,
next2: nil,
accept: "",
state: NODE_STATE,
anchor: NONE,
}
NODE_STATE = 1
return node
}
其中需要关注的是biteset,它用来实现字符串,如果一条边对应输入为[a-zA-Z],那么我们就把字符当做键,true当做值插入到这个map,我们还可以执行取反操作,例如[^a-zA-Z]表示匹配不是字母的字符,这样我们把值设置成false即可。
接下来我们看两个最关键,也是实现难度最大的组件,第一个类似于我们前面实现的lexer,它负责从输入文件中读取数据,然后将读到的字符转换成对应的token,与前面章节不同的是,我们这次一次只读取一个字符进行分析,创建名为LexReader.go文件,由于它的内容比较多,我们一点点展开,首先看其定义部分:
代码语言:javascript复制package nfa
import (
"bufio"
"fmt"
"os"
"strings"
"unicode"
)
type TOKEN int
const (
ASCII_CHAR_COUNT = 256
)
/*
我们需要对正则表达式字符串进行逐个字符解析,每次读取一个字符时将其转换成特定的token,
这里将不同字符对应的token定义出来
*/
const (
EOS TOKEN = iota //读到一行末尾
ANY // .
AT_BOL //^
AT_EOL //$
CCL_END // ]
CCL_START // [
CLOSE_CURLY // }
CLOSE_PARAN // )
CLOSURE //*
DASH //-
END_OF_INPUT // 文件末尾
L //字符常量
OPEN_CULY //{
OPEN_PAREN //(
OPTIONAL // ?
OR // |
PLUS_CLOSE //
)
type LexReader struct {
Verbose bool //打印辅助信息
ActualLineNo int //当前读取行号
LineNo int //如果表达式有多行,该变量表明当前读到第几行
InputFileName string //读取的文件名
Lexeme int //当前读取字符对应ASCII的数值
inquoted bool //读取到双引号之一
OutputFileName string
lookAhead uint8 //当前读取字符的数值
tokenMap []TOKEN //将读取的字符对应到其对应的token值
currentToken TOKEN //当前字符对应的token
scanner *bufio.Scanner //用于读取输入文件,我们需要一行行读取文件内容
macroMgr *MacroManager
currentInput string //当前读到的行
IFile *os.File //读入的文件
OFile *os.File //写出的文件
lineStack []string //用于对正则表达式中的宏定义进行展开
inComment bool //是否读取到了注释内容
}
func NewLexReader(inputFile string, outputFile string) (*LexReader, error) {
reader := &LexReader{
Verbose: true,
ActualLineNo: 0,
LineNo: 0,
InputFileName: inputFile,
OutputFileName: outputFile,
Lexeme: 0,
inquoted: false,
currentInput: "",
currentToken: EOS,
lineStack: make([]string, 0),
macroMgr: GetMacroManagerInstance(),
inComment: false,
}
var err error
reader.IFile, err = os.Open(inputFile)
reader.OFile, err = os.Create(outputFile)
if err == nil {
reader.scanner = bufio.NewScanner(reader.IFile)
}
reader.initTokenMap()
return reader, err
}
func (l *LexReader) initTokenMap() {
l.tokenMap = make([]TOKEN, ASCII_CHAR_COUNT)
for i := 0; i < len(l.tokenMap); i {
l.tokenMap[i] = L
}
//将对应字符对应到相应的token值
l.tokenMap[uint8('$')] = AT_EOL
l.tokenMap[uint8('(')] = OPEN_PAREN
l.tokenMap[uint8(')')] = CLOSE_PARAN
l.tokenMap[uint8('*')] = CLOSURE
l.tokenMap[uint8(' ')] = PLUS_CLOSE
l.tokenMap[uint8('-')] = DASH
l.tokenMap[uint8('.')] = ANY
l.tokenMap[uint8('?')] = OPTIONAL
l.tokenMap[uint8('[')] = CCL_START
l.tokenMap[uint8(']')] = CCL_END
l.tokenMap[uint8('^')] = AT_BOL
l.tokenMap[uint8('{')] = OPEN_CULY
l.tokenMap[uint8('|')] = OR
l.tokenMap[uint8('}')] = CLOSE_CURLY
}
接下来我们要实现的函数,就是将输入函数中%{ %}对应部分直接拷贝到输出文件,我们使用Head函数来实现,其代码如下:
代码语言:javascript复制func (l *LexReader) Head() {
/*
读取和解析宏定义部分
*/
transparent := false
for l.scanner.Scan() {
l.ActualLineNo = 1
l.currentInput = l.scanner.Text()
if l.Verbose {
fmt.Printf("h%d: %sn", l.ActualLineNo, l.currentInput)
}
if l.currentInput[0] == '%' {
if l.currentInput[1] == '%' {
//头部读取完毕
l.OFile.WriteString("n")
break
} else {
if l.currentInput[1] == '{' {
//拷贝头部代码
transparent = true
} else if l.currentInput[1] == '}' {
//头部代码拷贝完毕
transparent = false
} else {
err := fmt.Sprintf("illegal directive :%s n", l.currentInput[1])
panic(err)
}
}
} else if transparent || l.currentInput[0] == ' ' {
l.OFile.WriteString(l.currentInput "n")
} else {
//解析宏定义
l.macroMgr.NewMacro(l.currentInput)
l.OFile.WriteString("n")
}
}
if l.Verbose {
//将当前解析的宏定义打印出来
l.printMacs()
}
}
func (l *LexReader) printMacs() {
l.macroMgr.PrintMacs()
}
func (l *LexReader) Match(t TOKEN) bool {
return l.currentToken == t
}
在Head函数中,它首先一行行读取输入文件的内容,然后对输入内容进行识别,如果发现读入的行中出现”%%”,这意味着读取任务结束,也就是输入文件的头部内容已经被读取完毕。如果首先读取到符号%,然后接着读取到{,那么我们进入拷贝模式,也就是把所有内容拷贝到输出文件,直到遇到符号 %}为止。一旦读取到%}之后,代码进入到宏定义的识别过程,所有内容在读取符号%%之前都对应宏定义,此时每读取一行内容都是宏定义,代码讲读取的内容输入到l.macroMgr.NewMacro(l.currentInput),它会将读取内容分解成宏定义的名称和内容然后存储起来。
接下来我们进入到最为复杂的一个函数,它一次读取一个字符,然后将字符转换成对应token,它之所以复杂和繁琐就是因为要处理一些特殊情况,我们先看代码实现:
代码语言:javascript复制func (l *LexReader) Advance() TOKEN {
/*
一次读取一个字符然后判断其所属类别,麻烦在于处理转义符和双引号,
如果读到 "s"那么我们要将其对应到空格
*/
sawEsc := false //释放看到转义符
parseErr := NewParseError()
macroMgr := GetMacroManagerInstance()
if l.currentToken == EOS {
if l.inquoted {
parseErr.ParseErr(E_NEWLINE)
}
l.currentInput = l.GetExpr()
if len(l.currentInput) == 0 {
l.currentToken = END_OF_INPUT
return l.currentToken
}
}
/*
在解析正则表达式字符串时,我们会遇到宏定义,例如:
{D}*{D}
当我们读取到最左边的{时,我们需要将D替换成[0-9],此时我们需要先将后面的字符串加入栈,
也就是将字符串"*{D}"放入lineStack,然后讲D转换成[0-9],接着解析字符串"[0-9]",
解析完后再讲原来放入栈的字符串拿出来继续解析
*/
for len(l.currentInput) == 0 {
if len(l.lineStack) == 0 {
l.currentToken = EOS
return l.currentToken
} else {
l.currentInput = l.lineStack[len(l.lineStack)-1]
l.lineStack = l.lineStack[0 : len(l.lineStack)-1]
}
}
if !l.inquoted {
for l.currentInput[0] == '{' { //宏定义里面可能还会嵌套宏定义
//此时需要展开宏定义
l.currentInput = l.currentInput[1:]
expandedMacro := macroMgr.ExpandMacro(l.currentInput)
var i int
for i = 0; i < len(l.currentInput); i {
if l.currentInput[i] == '}' {
break
}
}
l.lineStack = append(l.lineStack, l.currentInput[i 1:])
l.currentInput = expandedMacro
}
}
if l.currentInput[0] == '"' {
l.inquoted = !l.inquoted
l.currentInput = l.currentInput[1:]
if len(l.currentInput) == 0 {
l.currentToken = EOS
return l.currentToken
}
}
sawEsc = l.currentInput[0] == '\'
if !l.inquoted {
if l.currentInput[0] == ' ' {
l.currentToken = EOS
return l.currentToken
}
/*
一行内容分为两部分,用空格隔开,前半部分是正则表达式,后半部分是匹配后应该执行的代码
这里读到第一个空格表明我们完全读取了前半部分,也就是描述正则表达式的部分
*/
l.Lexeme = l.esc()
} else {
if sawEsc && l.currentInput[1] == '"' {
//双引号被转义
l.currentInput = l.currentInput[2:]
l.Lexeme = int('"')
} else {
l.Lexeme = int(l.currentInput[0])
l.currentInput = l.currentInput[1:]
}
}
if l.inquoted || sawEsc {
l.currentToken = L
} else {
l.currentToken = l.tokenMap[l.Lexeme]
}
return l.currentToken
}
这个函数的实现较为复杂,基本逻辑是每次从输入文件中读入一行,然后针对读入的字符串逐个字符分析,这里有几个点需要注意,第一是读取到宏定义时我们需要进行替换,它对应代码为:
代码语言:javascript复制for l.currentInput[0] == '{' { //宏定义里面可能还会嵌套宏定义
//此时需要展开宏定义
l.currentInput = l.currentInput[1:]
expandedMacro := macroMgr.ExpandMacro(l.currentInput)
var i int
for i = 0; i < len(l.currentInput); i {
if l.currentInput[i] == '}' {
break
}
}
l.lineStack = append(l.lineStack, l.currentInput[i 1:])
l.currentInput = expandedMacro
}
一旦读取到左大括号时,上面代码片段就会被执行,它会将大括号里面的字符串取出并将其当做宏定义的名字,然后将宏定义后面的字符串先压入堆栈,然后取出宏对应的内容进行解析。例如读取表达式“(e{D} )?”,一旦读取到第一个左大括号时,它会把字符D抽取出来,然后将右括号后面的字符串” )?”压入堆栈,然后获取D对应的定义,也就是字符串”[0-9]”进行解析,一旦这个字符串解析完成后,再从堆栈中取出被压入的内容继续解析,下面代码片段对应从堆栈获取压入的字符串:
代码语言:javascript复制for len(l.currentInput) == 0 {
if len(l.lineStack) == 0 {
l.currentToken = EOS
return l.currentToken
} else {
l.currentInput = l.lineStack[len(l.lineStack)-1]
l.lineStack = l.lineStack[0 : len(l.lineStack)-1]
}
}
如果被压入的字符串已经没有内容,那么我们就继续从堆栈上取出内容进行解析。另外还需要考虑的是宏定义里面可能还会包含宏定义,例如:
代码语言:javascript复制D [0-9]
DD {D}
上面的定义是合法的,一旦程序解读到DD的时候,它会取出对应内容也就是”{D}”,此时它发现左大括号,于是它再次将括号内的字符串取出,然后将其当做宏定义的名称去查找对应内容,这个操作对应如下片段:
代码语言:javascript复制if !l.inquoted {
for l.currentInput[0] == '{' { //宏定义里面可能还会嵌套宏定义
//此时需要展开宏定义
l.currentInput = l.currentInput[1:]
expandedMacro := macroMgr.ExpandMacro(l.currentInput)
var i int
for i = 0; i < len(l.currentInput); i {
if l.currentInput[i] == '}' {
break
}
}
l.lineStack = append(l.lineStack, l.currentInput[i 1:])
l.currentInput = expandedMacro
}
}
输入解析过程有一些特定情况需要考虑,那就是遇到双引号或者转义符,任何出现在双引号中的字符我们都当做普通字符处理,例如”{D}”,这种情况我们把{D}看做是一个字符串而不是宏定义,同时一旦出现转义符我们也要特殊处理,例如{D},由于左括号和右括号前面都有转义符””,因此我们把这两个括号当做普通字符处理,这些情况也是上面代码中两个变量inquoted, 和sawEsc的作用。
代码中有几个变量需要关注,首先是currentToken,它对应当前读到字符对应的token,如果当前读入的字符串已经解析完毕,那么它的值变为EOS(end of string),另一个变量是Lexeme,它对应当前读入字符的ASCII码数值,currentInput对应当前读入的行。
在上面实现中有一个函数调用需要关注,那就是esc,它的作用是将特定字符进行转义,我们看其实现:
代码语言:javascript复制func (l *LexReader) esc() int {
/*
该函数将转义符转换成对应ASCII码并返回,如果currentInput对应的第一个字符不是反斜杠,那么它直接返回第一个字符
然后currentInput递进一个字符。下列转义符将会被处理
b backspace
f formfeed
n newline
r carriage return
t tab
e ESC字符 对应('