自己动手写编译器:代码实现正则表达式到NFA状态机

在编译器开发中有两个非常重要的工具名为lex和yacc,他们是编译器的生成器。本质上我们不需要一行行去完成编译器的代码,只需要借助这两个工具,同时制定好词法解析和语法解析的规则后,这两个工具就会自动帮我们把代码生成,我们后续的任务就是使用go语言将这两个工具实现。

为了更好的理解我们要开发的GoLex,我们先熟悉一下lex工具的使用。在Centos上安装lex的命令为:

yum install flex

接下来我们看看它的使用。lex的作用主要是根据给定正则表达式,然后lex会把既定的正则表达式生成成对应的C语言代码,我们将生成的代码编译后就能得到可以针对输入进行相应识别的程序,我们看看一个具体例子。安装完flex后,我们在本地创建一个名为ch1-02.l的文件,然后输入内容如下:

%{
   /*
   这里是一段直接拷贝到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 verb\n", yytext);}
{alpha}+ {printf("%s: is not a verb\n", 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通过识别正则表达式,构造状态机,然后将后者转换为可执行代码后对应的函数。

有了上面文件后,我们执行如下命令:

lex ch1-02.l

完成后就会在本地生成一个名为lex.yy.c的文件,里面的代码非常复杂,同时这些代码完全由lex生成。最后我们用如下命令编译:

cc lex.yy.c

然后会在本地生成可执行文件a.out,执行a.out后程序运行起来,然后我们就可以输入相应字符串,如果对应字符串满足给定正则表达式,例如输入字符串中包含"should", “would"等单词时,对应的语句printf(”%s: is a verb\n", yytext);就可以执行,情况如下图所示:
请添加图片描述
总结来说lex和yacc就是开发编译器的框架,他们负责识别和解析,然后把结果传递给我们提供的函数。接下来我们开发的GoLex就是我们当前介绍lex程序的go语言实现,同时我尝试把其中的C语言转换成python语言。

首先我们创建GoLex工程,然后在其中创建nfa文件夹,然后执行如下命令初始化:

go mod init nfa
go mod tidy

首先我们看看对应的.l文件,创建input.l,输入内容如下:

%{
    FCON = 1
    ICON = 2
%}
D  [0-9]
%%
(e{D}+)?
%%

我们将读取上面内容并根据给定的正则表达式创建NFA状态机。我们先在里面创建一些支持类功能的代码,第一个事debugger.go文件,它用来输出函数的调用信息,其内容如下:

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,其内容如下:

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,里面实现代码如下:

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中,后面在解析正则表达式时,一旦遇到宏定义,例如我们定义了宏定义:

D  [0-9]

然后在后续表达式中遇到宏定义时,例如:

(e{D}+)?

当读取到{D}时,我们会将其替换成[0-9],这个替换功能正是由ExpandMacro函数来实现。下面我们先定义状态机节点对应的数据结构,创建nfa.go,实现代码如下:

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文件,由于它的内容比较多,我们一点点展开,首先看其定义部分:

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函数来实现,其代码如下:

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: %s\n", 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,它之所以复杂和繁琐就是因为要处理一些特殊情况,我们先看代码实现:

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
}

这个函数的实现较为复杂,基本逻辑是每次从输入文件中读入一行,然后针对读入的字符串逐个字符分析,这里有几个点需要注意,第一是读取到宏定义时我们需要进行替换,它对应代码为:

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]"进行解析,一旦这个字符串解析完成后,再从堆栈中取出被压入的内容继续解析,下面代码片段对应从堆栈获取压入的字符串:

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]
		}
	}

如果被压入的字符串已经没有内容,那么我们就继续从堆栈上取出内容进行解析。另外还需要考虑的是宏定义里面可能还会包含宏定义,例如:

D   [0-9]
DD    {D}

上面的定义是合法的,一旦程序解读到DD的时候,它会取出对应内容也就是"{D}",此时它发现左大括号,于是它再次将括号内的字符串取出,然后将其当做宏定义的名称去查找对应内容,这个操作对应如下片段:

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,它的作用是将特定字符进行转义,我们看其实现:

func (l *LexReader) esc() int {
	/*
			该函数将转义符转换成对应ASCII码并返回,如果currentInput对应的第一个字符不是反斜杠,那么它直接返回第一个字符
		    然后currentInput递进一个字符。下列转义符将会被处理
		   \b  backspace
		   \f  formfeed
		   \n  newline
		   \r  carriage return
		   \t  tab
		   \e  ESC字符 对应('\0333')
		   \^C C是任何字母,它表示控制符
	*/
	var rval int
	if l.currentInput[0] != '\\' {
		rval = int(l.currentInput[0])
		l.currentInput = l.currentInput[1:]
	} else {
		l.currentInput = l.currentInput[1:] //越过反斜杠
		currentInputUpcase := strings.ToUpper(l.currentInput)
		switch currentInputUpcase[0] {
		case '\x00':
			rval = '\\'
		case 'B':
			rval = '\b'
		case 'F':
			rval = '\f'
		case 'N':
			rval = '\n'
		case 'R':
			rval = '\r'
		case 'S':
			rval = ' '
		case 'T':
			rval = '\t'
		case 'E':
			rval = '\033'
		case '^':
			l.currentInput = l.currentInput[1:]
			upperStr := strings.ToUpper(l.currentInput)
			rval = int(upperStr[0] - '@')
		case 'X':
			rval = 0
			savedCurrentInput := l.currentInput
			transformHex := false
			l.currentInput = l.currentInput[1:]
			if l.isHexDigit(l.currentInput[0]) {
				transformHex = true
				rval = int(l.hex2bin(l.currentInput[0]))
				l.currentInput = l.currentInput[1:]
			}
			if l.isHexDigit(l.currentInput[0]) {
				transformHex = true
				rval <<= 4
				rval |= int(l.hex2bin(l.currentInput[0]))
				l.currentInput = l.currentInput[1:]
			}
			if l.isHexDigit(l.currentInput[0]) {
				transformHex = true
				rval <<= 4
				rval |= int(l.hex2bin(l.currentInput[0]))
				l.currentInput = l.currentInput[1:]
			}
			if !transformHex {
				//如果接在X后面的不是合法16进制字符,那么我们仅仅忽略掉X即可
				l.currentInput = savedCurrentInput
			}
		default:
			if !l.isOctDigit(l.currentInput[0]) {
				rval = int(l.currentInput[0])
				l.currentInput = l.currentInput[1:]
			} else {
				l.currentInput = l.currentInput[1:]
				rval = int(l.oct2bin(l.currentInput[0]))
				savedCurrentInput := l.currentInput
				isTransformOct := false
				l.currentInput = l.currentInput[1:]
				if l.isOctDigit(l.currentInput[0]) {
					isTransformOct = true
					rval <<= 3
					rval |= int(l.oct2bin(l.currentInput[0]))
					l.currentInput = l.currentInput[1:]
				}
				if l.isOctDigit(l.currentInput[0]) {
					isTransformOct = true
					rval <<= 3
					rval |= int(l.oct2bin(l.currentInput[0]))
					l.currentInput = l.currentInput[1:]
				}
				if !isTransformOct {
					l.currentInput = savedCurrentInput
				}
			}
		}
	}

	return rval
}

func (l *LexReader) isHexDigit(x uint8) bool {
	return unicode.IsDigit(rune(x)) || ('a' <= x && x <= 'f') || ('A' <= x && x <= 'F')
}

func (l *LexReader) isOctDigit(x uint8) bool {
	return '0' <= x && x <= '7'
}

func (l *LexReader) hex2bin(x uint8) uint8 {
	/*
		将16进制字符转换为对应数值, x 必须必须是如下字符0123456789abcdefABCDEF
	*/
	var val uint8
	if unicode.IsDigit(rune(x)) {
		val = x - '0'
	} else {
		val = uint8(unicode.ToUpper(rune(x)-'A') & 0xf)
	}

	return val
}

func (l *LexReader) oct2bin(x uint8) uint8 {
	/*
		将十六进制的数字或字母转换为八进制数字,输入的x必须在范围'0'-'7'
	*/
	return (x - '0') & 0x7
}

上面代码实现中有两种情况需要关注,那就是对十六进制和八进制数的转义,当我们读取字符串"\X00f"时,上面代码会将其转换为十进制数15,如果没有x那么就会根据8进制转换,例如“\011"对应的十进制数值就是9,注意到上面代码最多对三个数字进行解析,其他字符的转义可以看代码中的注释。

此外我们看currentInput的设置,它通过调用GetExpr()来获取数据,我们看看这个函数的实现:

func (l *LexReader) GetExpr() string {
	/*
		一次从文本中读入一行字符串
	*/
	if l.Verbose {
		fmt.Printf("b:%d\n", l.ActualLineNo)
	}

	readLine := ""
	haveLine := l.scanner.Scan()
	for haveLine {
		currentLine := l.scanner.Text()
		haveLine = l.scanner.Scan()
		if len(strings.TrimSpace(currentLine)) == 0 {
			//忽略掉全是空格的一行
			continue
		}
		if currentLine[0] == uint8(' ') {
			/*
					一个正则表达式可能会分成几行出现,例如 ({D)+ | {D)*\.{D)+ | {D)+\.{D)*) (e{D}+)? 可能分成三行:
				    ({D)+ | {D)*\.{D)+
				       |
				       {D)+\.{D)*) (e{D}+)?

				   第二行和第三行都以空格开始,这种情况我们要将三行内容全部读取,然后合成一行
			*/
			readLine += strings.TrimSpace(currentLine)
		} else {
			readLine = currentLine
			break
		}

	}

	if l.Verbose {
		if !haveLine {
			fmt.Println("----EOF------")
		} else {
			fmt.Println(readLine)
		}
	}

	return readLine
}

上面代码实现中,通过scanner一次读取输入文件的一行文本内容,然后将读入的字符串去除前后空格,如果读入的一行全是由空格组成,那么我们就忽略掉这行并继续读取下一行,另外还需要考虑的是,一个正则表达式可能会分割成多行,那么我们需要将这些不同行的内容合并成一行。

以上内容就是针对输入的读取和解析,它对应于我们前面编译器实例中的词法解析流程。当我们获得输入后就需要识别输入是否满足给定规则,这部分对应前面编译器实例中的语法解析过程,由此我们进入解析过程的实现。

正则表达式字符串的解析跟我们前面编译器实现的语法解析流程一样,我们将字符串中的每个字符转换成对应token之后,就需要判断token的组合是否符合语法规则,由此我们首先给出正则表达式对应的语法规则:

        machine -> rule machine | rule END_OF_INPUT
		rule -> expr EOS action | '^'expr EOS action | expr '$' EOS action
		action -> white_space string | white_space | ε
		expr -> expr '|' cat_expr  | cat_expr
		cat_expr -> cat_expr factor | factor
		factor -> term* | term+ | term? | term
		term -> '['string']' | '[' '^' string ']' | '[' ']' | ’[' '^' ']' | '.' | character | '(' expr ')'
		white_space -> 匹配一个或多个空格或tab
		character -> 匹配任何一个除了空格外的ASCII字符
		string -> 由ASCII字符组合成的字符串

我们看看上面的语法规则如何解析给定正则表达式字符串,例如给定字符串"(e{D}+)“,它经过LexReader的处理后{D}会被替换成[0-9],于是字符串会变成”(e[0-9]+)",他们转换成token后为: LEFT_PARAN L CCL_START L DASH L CCL_END PLUS_CLOSURE RIGHT_PARAN , 表达式中的字符常量都会转换成L。

我们看看如何使用上面的语法规则解析上面的token序列。首先进入规则machine,它的右边开始是规则rule,因此继续进入到rule。rule规则的右边以expr开始,因此继续进入到规则expr。由于表达式中没有符号’|‘,因此进入到expr规则右边的规则cat_expr。在cat_expr中我们会继续进入factor,由于字符串中没有包含符号*, + 和?,因此下一步进入规则term,在规则term中,由于我们第一个字符是左括号,因此此时要匹配规则’(’ expr ‘)’,于是这里我们去除掉标签LEFT_PAREN,然后继续进入到规则expr进行后续匹配。大家可以把上面对正则表达式的识别跟前面我们对四则混合运算表达式的识别对比看看,其实本质上是一样的,符号’|‘对应运算表达式中的’+‘和’-‘,两个表达式前后相连对应计算表达式中的’*‘和’’

不知道大家是否感觉到,所谓语言其实就是提出这一系列解析规则,假如你要开发一门新语言,你只要设计出一组类似这样的语法解析规则就可以了,剩下的就是工程上的开发流程而已。我们看看语法解析的具体实现,创建文件RegParser.go,实现代码如下:

package nfa

import (
	"fmt"
)

const (
	ASCII_CHAR_NUM = 256
)

type RegParser struct {
	debugger  *Debugger
	parseErr  *ParseError
	lexReader *LexReader
	//用于打印NFA状态机信息
	visitedMap map[*NFA]bool
	stateNum   int
}

func NewRegParser(reader *LexReader) (*RegParser, error) {
	regReader := &RegParser{
		debugger:   newDebugger(),
		parseErr:   NewParseError(),
		lexReader:  reader,
		visitedMap: make(map[*NFA]bool),
		stateNum:   0,
	}

	return regReader, nil
}

func (r *RegParser) Parse() *NFA {
	r.lexReader.Advance()
	return r.machine()
}

func (r *RegParser) machine() *NFA {
	/*
		这里进入到正则表达式的解析,其语法规则如下:
		machine -> rule machine | rule END_OF_INPUT
		rule -> expr EOS action | '^'expr EOS action | expr '$' EOS action
		action -> white_space string | white_space | ε
		expr -> expr '|' cat_expr  | cat_expr
		cat_expr -> cat_expr factor | factor
		factor -> term* | term+ | term? | term
		term -> '['string']' | '[' '^' string ']' | '[' ']' | ’[' '^' ']' | '.' | character | '(' expr ')'
		white_space -> 匹配一个或多个空格或tab
		character -> 匹配任何一个除了空格外的ASCII字符
		string -> 由ASCII字符组合成的字符串
	*/
	var p *NFA
	var start *NFA

	r.debugger.Enter("machine")

	start = NewNFA()
	p = start
	p.next = r.rule()

	for !r.lexReader.Match(END_OF_INPUT) {
		p.next2 = NewNFA()
		p = p.next2
		p.next = r.rule()
	}

	r.debugger.Leave("machine")

	return start
}

func (r *RegParser) rule() *NFA {
	/*
		rule -> expr EOS action
		     ->^ expr EOS action
		     -> expr $ EOS action

		action -> <tabs> <characters> epsilon
	*/
	var start *NFA
	var end *NFA
	anchor := NONE

	r.debugger.Enter("rule")

	if r.lexReader.Match(AT_BOL) {
		/*
			当前读到符号 ^,必须开头匹配,因此首先需要读入一个换行符,这样才能确保接下来的字符起始于新的一行
		*/
		start = NewNFA()
		start.edge = EdgeType('\n')
		anchor |= START
		r.lexReader.Advance()
		start, end = r.expr(start.next, end)
	} else {
		start, end = r.expr(start, end)
	}

	if r.lexReader.Match(AT_EOL) {
		/*
			读到符号$,必须是字符串的末尾匹配,因此匹配后接下来必须是回车换行符号,要不然
			无法确保匹配的字符串在一行的末尾
		*/
		r.lexReader.Advance()
		end.next = NewNFA()
		end.edge = CCL //边对应字符集,其中包含符号/r, /n
		end.bitset["\r"] = true
		end.bitset["\n"] = true

		end = end.next
		anchor |= END
	}

	end.accept = r.lexReader.currentInput
	end.anchor = anchor
	r.lexReader.Advance()

	r.debugger.Leave("rule")
	return start
}

func (r *RegParser) expr(start *NFA, end *NFA) (newStar *NFA, newEnd *NFA) {
	/*
		expr -> expr or expr | cat_expr
		一个正则表达式可以分解成两个表达式的并,或是两个表达式的前后连接
	*/
	e2Start := NewNFA()
	e2End := NewNFA()
	var p *NFA
	r.debugger.Enter("expr")

	start, end = r.catExpr(start, end)

	for r.lexReader.Match(OR) {
		r.lexReader.Advance()
		e2Start, e2End = r.catExpr(e2Start, e2End)
		p = NewNFA()
		p.next2 = e2Start
		p.next = start
		start = p

		p = NewNFA()
		end.next = p
		e2End.next = p
		end = p
	}

	r.debugger.Leave("expr")

	return start, end
}

func (r *RegParser) catExpr(start *NFA, end *NFA) (newStart *NFA, newEnd *NFA) {
	/*
		cat_expr -> cat_expr | factor
	*/
	e2Start := NewNFA()
	e2End := NewNFA()
	r.debugger.Enter("catExpr")

	//判断起始字符是否合法
	if r.firstInCat(r.lexReader.currentToken) {
		start, end = r.factor(start, end)
	}

	for r.firstInCat(r.lexReader.currentToken) {
		e2Start, e2End = r.factor(e2Start, e2End)
		end.next = e2Start
		end = e2End
	}

	r.debugger.Leave("catExpr")
	return start, end
}

func (r *RegParser) firstInCat(tok TOKEN) bool {
	switch tok {
	case CLOSE_PARAN:
		fallthrough
	case AT_EOL:
		fallthrough
	case OR:
		fallthrough
	case EOS:
		//这些符号表明正则表达式停止了前后连接过程
		return false
	case CLOSURE:
		fallthrough
	case PLUS_CLOSE:
		fallthrough
	case OPTIONAL:
		//这些字符必须跟在表达式后边而不是作为起始符号
		r.parseErr.ParseErr(E_CLOSE)
		return false
	case CCL_END:
		r.parseErr.ParseErr(E_BRACKET)
		return false
	case AT_BOL:
		r.parseErr.ParseErr(E_BOL)
		return false
	}

	return true
}

func (r *RegParser) factor(start *NFA, end *NFA) (newStart *NFA, newEnd *NFA) {
	/*
		factor -> term* | term+ | term?
	*/
	r.debugger.Enter("factor")
	var e2Start *NFA
	var e2End *NFA
	start, end = r.term(start, end)
	e2Start = start
	e2End = end
	if r.lexReader.Match(CLOSURE) || r.lexReader.Match(PLUS_CLOSE) || r.lexReader.Match(OPTIONAL) {
		e2Start = NewNFA()
		e2End = NewNFA()
		e2Start.next = start
		end.next = e2End

		if r.lexReader.Match(CLOSURE) || r.lexReader.Match(OPTIONAL) {
			//匹配操作符*,+,创建一条epsilon边直接连接头尾
			e2Start.next2 = e2End
		}

		if r.lexReader.Match(CLOSURE) || r.lexReader.Match(PLUS_CLOSE) {
			//匹配操作符*,?,创建一条epsilon边连接尾部和头部
			end.next2 = start
		}

		r.lexReader.Advance()
	}
	r.debugger.Leave("factor")
	return e2Start, e2End
}

func (r *RegParser) printCCL(set map[string]bool) {
	//输出字符集的内容
	s := fmt.Sprintf("%s", "[")
	for i := 0; i <= 127; i++ {
		selected, ok := set[string(i)]
		if !ok {
			continue
		}

		if !selected {
			continue
		}

		if i < int(' ') {
			//控制字符
			s += fmt.Sprintf("^%s", string(i+int('@')))
		} else {
			s += fmt.Sprintf("%s", string(i))
		}
	}

	s += "]"
	fmt.Println(s)
}

func (r *RegParser) PrintNFA(start *NFA) {
	fmt.Println("----------NFA INFO------------")
	nfaNodeStack := make([]*NFA, 0)
	nfaNodeStack = append(nfaNodeStack, start)
	containsMap := make(map[*NFA]bool)
	containsMap[start] = true

	for len(nfaNodeStack) > 0 {
		node := nfaNodeStack[len(nfaNodeStack)-1]
		nfaNodeStack = nfaNodeStack[0 : len(nfaNodeStack)-1]
		fmt.Printf("\n----------In node with state number: %d-------------------\n", node.state)
		r.printNodeInfo(node)

		if node.next != nil && !containsMap[node.next] {
			nfaNodeStack = append(nfaNodeStack, node.next)
			containsMap[node.next] = true
		}

		if node.next2 != nil && !containsMap[node.next2] {
			nfaNodeStack = append(nfaNodeStack, node.next2)
			containsMap[node.next2] = true
		}
	}
}

func (r *RegParser) printNodeInfo(node *NFA) {
	if node.next == nil {
		fmt.Println("this node is TERMINAL")
		return
	}
	fmt.Println("****Edge Info****")
	r.printEdge(node)
	if node.next != nil {
		fmt.Printf("Next node is :%d\n", node.next.state)
	}

	if node.next2 != nil {
		fmt.Printf("Next ode is :%d\n", node.next2.state)
	}
}

func (r *RegParser) printEdge(node *NFA) {
	switch node.edge {
	case CCL:
		r.printCCL(node.bitset)
	case EPSILON:
		fmt.Println("EPSILON")
	default:
		//匹配单个字符
		fmt.Printf("%s\n", string(node.edge))
	}
}

func (r *RegParser) doDash(set map[string]bool) {
	var first int
	for !r.lexReader.Match(EOS) && !r.lexReader.Match(CCL_END) {
		if !r.lexReader.Match(DASH) {
			first = r.lexReader.Lexeme
			set[string(r.lexReader.Lexeme)] = true
		} else {
			r.lexReader.Advance() //越过 '-'
			for ; first <= r.lexReader.Lexeme; first++ {
				set[string(first)] = true
			}
		}
		r.lexReader.Advance()
	}
}

func (r *RegParser) term(start *NFA, end *NFA) (newStart *NFA, newEnd *NFA) {
	/*
		term -> [...] | [^...] | [] | [^] | . | (expr) | <character>
		[] 匹配空格,回车,换行,但不匹配\r
	*/
	r.debugger.Enter("term")

	if r.lexReader.Match(OPEN_PAREN) {
		//匹配(expr)
		r.lexReader.Advance()
		start, end = r.expr(start, end)
		if r.lexReader.Match(CLOSE_PARAN) {
			r.lexReader.Advance()
		} else {
			//没有右括号
			r.parseErr.ParseErr(E_PAREN)
		}
	} else {
		start = NewNFA()
		end = NewNFA()
		start.next = end

		if !(r.lexReader.Match(ANY) || r.lexReader.Match(CCL_START)) {
			//匹配单字符
			start.edge = EdgeType(r.lexReader.Lexeme)
			r.lexReader.Advance()
		} else {
			/*
				匹配 "." 本质上是匹配字符集,集合里面包含所有除了\r, \n 之外的ASCII字符
			*/
			start.edge = CCL
			if r.lexReader.Match(ANY) {
				for i := 0; i < ASCII_CHAR_NUM; i++ {
					if i != int('\r') && i != int('\n') {
						start.bitset[string(i)] = true
					}
				}
			} else {
				/*
					匹配由中括号形成的字符集
				*/
				r.lexReader.Advance() //越过'['
				negativeClass := false
				if r.lexReader.Match(AT_BOL) {
					/*
						[^...] 匹配字符集取反

					*/
					start.bitset[string('\n')] = false
					start.bitset[string('\r')] = false
					negativeClass = true
				}
				if !r.lexReader.Match(CCL_END) {
					/*
						匹配类似[a-z]这样的字符集
					*/
					r.doDash(start.bitset)
				} else {
					/*
						匹配 【】 或 [^]
					*/
					for c := 0; c <= int(' '); c++ {
						start.bitset[string(c)] = true
					}
				}

				if negativeClass {
					for key, _ := range start.bitset {
						start.bitset[key] = false
					}

					for i := 0; i <= 127; i++ {
						_, ok := start.bitset[string(i)]
						if !ok {
							start.bitset[string(i)] = true
						}
					}
				}

				r.lexReader.Advance() //越过 ']'
			}
		}
	}

	r.debugger.Leave("term")
	return start, end
}

代码实现的逻辑较为复杂,最好通过视频查看我调试过程才好理解。有几点需要注意,首先是firstInCat,它用来检测表达式的起始字符是否合法,有些字符不能出现在表达式的开始,例如’)’ ']'等,一旦发现他们出现在开头就意味着表达式的字符串有错误。对上面代码逻辑更详细的拆解请在B站搜索coding迪斯尼。

完成以上代码后,我们就完成了Lex程序的第一部分,后续还有很多工作需要处理,我们在后续章节再进行详解。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本书系统介绍了经典的编译理论和技术,同时也包含了面向对象语言等当前较新语言的编译技术。本书更可贵之处在于提供了较完整的适用于教学实践的样例语言,是一本理论和实践内容相结合的、不可多得的好书。 本书可用作大专院校教材、教师参考书以及编译器研究人员的参考资料。 目 录 译者序 前言 第1章 概论 1 1.1 为什么要用编译器 2 1.2 与编译器相关的程序 3 1.3 翻译步骤 5 1.4 编译器中的主要数据结构 8 1.5 编译器结构中的其他问题 10 1.6 自举与移植 12 1.7 TINY样本语言与编译器 14 1.7.1 TINY语言 15 1.7.2 TINY编译器 15 1.7.3 TM机 17 1.8 C-Minus:编译器项目的一种语言 18 练习 19 注意与参考 20 第2章 词法分析 21 2.1 扫描处理 21 2.2 正则表达式 23 2.2.1 正则表达式的定义 23 2.2.2 正则表达式的扩展 27 2.2.3 程序设计语言记号的正则表达式 29 2.3 有穷自动机 32 2.3.1 确定性有穷自动机的定义 32 2.3.2 先行、回溯和非确定性自动机 36 2.3.3 用代码实现有穷自动机 41 2.4 从正则表达式到DFA 45 2.4.1 从正则表达式NFA 45 2.4.2 从NFA到DFA 48 2.4.3 利用子集构造模拟NFA 50 2.4.4 将DFA中的状态数最小化 51 2.5 TINY扫描程序的实现 52 2.5.1 为样本语言TINY实现一个扫描 程序 53 2.5.2 保留字与标识符 56 2.5.3 为标识符分配空间 57 2.6 利用Lex 自动生成扫描程序 57 2.6.1 正则表达式的Lex 约定 58 2.6.2 Lex输入文件的格式 59 2.6.3 使用Lex的TINY扫描程序 64 练习 65 编程练习 67 注意与参考 67 第3章 上下文无关文法及分析 69 3.1 分析过程 69 3.2 上下文无关文法 70 3.2.1 与正则表达式比较 70 3.2.2 上下文无关文法规则的说明 71 3.2.3 推导及由文法定义的语言 72 3.3 分析树与抽象语法树 77 3.3.1 分析树 77 3.3.2 抽象语法树 79 3.4 二义性 83 3.4.1 二义性文法 83 3.4.2 优先权和结合性 85 3.4.3 悬挂else问题 87 3.4.4 无关紧要的二义性 89 3.5 扩展的表示法:EBNF和语法图 89 3.5.1 EBNF表示法 89 3.5.2 语法图 91 3.6 上下文无关语言的形式特性 93 3.6.1 上下文无关语言的形式定义 93 3.6.2 文法规则和等式 94 3.6.3 乔姆斯基层次和作为上下文无关 规则的语法局限 95 3.7 TINY语言的语法 97 3.7.1 TINY的上下文无关文法 97 3.7.2 TINY编译器的语法树结构 98 练习 101 注意与参考 104 第4章 自顶向下的分析 105 4.1 使用递归下降分析算法进行自顶向下 的分析 105 4.1.1 递归下降分析的基本方法 105 4.1.2 重复和选择:使用EBNF 107 4.1.3 其他决定问题 112 4.2 LL(1)分析 113 4.2.1 LL(1)分析的基本方法 113 4.2.2 LL(1)分析与算法 114 4.2.3 消除左递归和提取左因子 117 4.2.4 在LL(1)分析中构造语法树 124 4.3 First集合和Follow集合 125 4.3.1 First 集合 125 4.3.2 Follow 集合 130 4.3.3 构造LL(1)分析表 134 4.3.4 再向前:LL(k)分析程序 135 4.4 TINY语言的递归下降分析程序 136 4.5 自顶向下分析程序中的错误校正 137 4.5.1 在递归下降分析程序中的错误 校正 138 4.5.2 在LL(1)分析程序中的错误校正 140 4.5.3 在TINY分析程序中的错误校正 141 练习 143 编程练习 146 注意与参考 148 第5章 自底向上的分析 150 5.1 自底向上分析概览 151 5.2 LR(0)项的有穷自动机与LR(0)分析 153 5.2.1 LR(0)项 153 5.2.2 项目的有穷自动机 154 5.2.3 LR(0)分析算法 157 5.3 SLR(1)分析 160 5.3.1 SLR(1)分析算法 160 5.3.2 用于分析冲突的消除二义性 规则 163 5.3.3 SLR(1)分析能力的局限性 164 5.3.4 SLR(k)文法 165 5.4 一般的LR(1)和LALR(1)分析 166 5.4.1 LR(1)项的有穷自动机 166 5.4.2 LR(1)分析算法 169 5.4.3 LALR(1)分析 171 5.5 Yacc:一个LALR(1)分析程序的 生成器 173 5.5.1 Yacc基础 173 5.5.2 Yacc选项 176 5.5.3 分析冲突与消除二义性的规则 180 5.5.4 描述Yacc分析程序的执行 183 5.5.5 Yacc中的任意值类型 184 5.5.6 Yacc中嵌入的动作 185 5.6 使用Yacc生成TINY分析程序 186 5.7 自底向上分析程序中的错误校正 188 5.7.1 自底向上分析中的错误检测 188 5.7.2 应急方式错误校正 188 5.7.3 Yacc中的错误校正 189 5.7.4 TINY中的错误校正 192 练习 192 编程练习 195 注意与参考 197 第6章 语义分析 198 6.1 属性和属性文法 199 6.1.1 属性文法 200 6.1.2 属性文法的简化和扩充 206 6.2 属性计算算法 207 6.2.1 相关图和赋值顺序 208 6.2.2 合成和继承属性 212 6.2.3 作为参数和返回值的属性 219 6.2.4 使用扩展数据结构存储属性值 221 6.2.5 语法分析时属性的计算 223 6.2.6 语法中属性计算的相关性 226 6.3 符号表 227 6.3.1 符号表的结构 228 6.3.2 说明 230 6.3.3 作用域规则和块结构 232 6.3.4 同层说明的相互作用 236 6.3.5 使用符号表的属性文法的一个 扩充例子 237 6.4 数据类型和类型检查 241 6.4.1 类型表达式和类型构造器 242 6.4.2 类型名、类型说明和递归类型 246 6.4.3 类型等价 248 6.4.4 类型推论和类型检查 253 6.4.5 类型检查的其他主题 255 6.5 TINY语言的语义分析 257 6.5.1 TINY的符号表 258 6.5.2 TINY语义分析程序 259 练习 260 编程练习 264 注意与参考 264 第7章 运行时环境 266 7.1 程序执行时的存储器组织 266 7.2 完全静态运行时环境 269 7.3 基于栈的运行时环境 271 7.3.1 没有局部过程的基于栈的环境 271 7.3.2 带有局部过程的基于栈的环境 281 7.3.3 带有过程参数的基于栈的环境 284 7.4 动态存储器 286 7.4.1 完全动态运行时环境 286 7.4.2 面向对象的语言中的动态存储器 287 7.4.3 堆管理 289 7.4.4 堆的自动管理 292 7.5 参数传递机制 292 7.5.1 值传递 293 7.5.2 引用传递 294 7.5.3 值结果传递 295 7.5.4 名字传递 295 7.6 TINY语言的运行时环境 296 练习 297 编程练习 303 注意与参考 304 第8章 代码生成 305 8.1 中间代码和用于代码生成的数据 结构 305 8.1.1 三地址码 306 8.1.2 用于实现三地址码的数据结构 308 8.1.3 P-代码 310 8.2 基本的代码生成技术 312 8.2.1 作为合成属性的中间代码或目标 代码 312 8.2.2 实际的代码生成 314 8.2.3 从中间代码生成目标代码 317 8.3 数据结构引用的代码生成 319 8.3.1 地址计算 319 8.3.2 数组引用 320 8.3.3 栈记录结构和指针引用 325 8.4 控制语句和逻辑表达式的代码生成 328 8.4.1 if 和while 语句的代码生成 328 8.4.2 标号的生成和回填 330 8.4.3 逻辑表达式的代码生成 330 8.4.4 if 和while 语句的代码生成过程 样例 331 8.5 过程和函数调用的代码生成 334 8.5.1 过程和函数的中间代码 334 8.5.2 函数定义和调用的代码生成过程 336 8.6 商用编译器中的代码生成:两个案 例研究 339 8.6.1 对于80×86的Borland 3.0版C编 译器 339 8.6.2 Sun SparcStation的Sun 2.0 C编 译器 343 8.7 TM:简单的目标机器 346 8.7.1 Tiny Machine的基本结构 347 8.7.2 TM模拟器 349 8.8 TINY语言的代码生成器 351 8.8.1 TINY代码生成器的TM接口 351 8.8.2 TINY代码生成器 352 8.8.3 用TINY编译器产生和使用TM 代码文件 354 8.8.4 TINY编译器生成的TM代码文 件示例 355 8.9 代码优化技术考察 357 8.9.1 代码优化的主要来源 358 8.9.2 优化分类 360 8.9.3 优化的数据结构和实现技术 362 8.10 TINY代码生成器的简单优化 366 8.10.1 将临时变量放入寄存器 366 8.10.2 在寄存器中保存变量 367 8.10.3 优化测试表达式 367 练习 368 编程练习 371 注意与参考 372 附录A 编译器设计方案 373 附录B 小型编译器列表 381 附录C Tiny Machine模拟器列表 417
编译原理中的正则表达式NFA(非确定有限自动机)的转换是将一个正则表达式转换成等价的、描述相同语言的NFA的过程。 在转换过程中,首先需要定义正则表达式的基本操作,包括连接、选择和闭包三种。然后,根据这些基本操作,通过递归的方式将正则表达式转换为NFA。 具体过程如下: 1. 基础操作: - 连接:对于正则表达式r1、r2,连接操作将其转换为一个新的NFA,该NFA包含r1和r2的所有状态,并且r1的终止状态的空转移边指向r2的起始状态。 - 选择:对于正则表达式r1、r2,选择操作将其转换为一个新的NFA,该NFA包含r1和r2的所有状态,并且新增加一个起始状态一个终止状态,起始状态通过空转移边分别指向r1和r2的起始状态,而r1和r2的终止状态通过空转移边指向新的终止状态。 - 闭包:对于正则表达式r,闭包操作将其转换为一个新的NFA,该NFA包含r的所有状态,并且新增加一个起始状态一个终止状态,起始状态通过空转移边指向r的起始状态,而r的终止状态通过空转移边分别指向起始状态和新的终止状态。 2. 利用基本操作将正则表达式转换为NFA的过程: - 将正则表达式转换为后缀表达式。 - 利用后缀表达式构建NFA的过程中,可以使用栈来辅助计算。 - 遍历后缀表达式的每一个字符: - 如果是操作数,则将其转换为NFA,并将其入栈。 - 如果是操作符,则从栈中弹出相应数量的NFA,按照该操作符进行基本操作,并将结果NFA入栈。 - 最终栈中只剩下一个NFA,即为转换结果。 通过上述过程,就可以将正则表达式转换为NFA代码形式。根据具体的编程语言和编译器实现,可以将其转换为相应的数据结构和算法,从而实现正则表达式的匹配和其他操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值