首页 > 其他分享 >自己动手写编译器:使用NFA识别字符串

自己动手写编译器:使用NFA识别字符串

时间:2023-06-14 11:33:44浏览次数:44  
标签:closure NFA epsilon move 编译器 result 字符串 input


在前面章节中我们构建了NFA状态机,现在我们看看如何使用它来识别给定字符串是否合法。首先我们先构造如下正则表达式对应的NFA,在input文件的表达式部分输入:

({D}*\.{D} | {D}\.{D}*)

这个表达式的目的是识别浮点数,用我们前面做好的代码生成的NFA状态机如下:

 

自己动手写编译器:使用NFA识别字符串_结点

 

这里我们需要引入两个个概念及其对应操作,首先是epsilon-clousure操作, 它表示给定一系列初始状态后,然后找到从这些状态出发通过epsilon边所能抵达的状态集合,例如epsilon-closure(0)={0,27,11,9,12,13,19},从上图看出,状态0开始经过epsilon边后能抵达点27,然后从点27经过epsilon边又能抵达点11,19,依此类推,这里需要注意的是episilon-closure的结果包含其输入的状态,例如epsilon-closure(0)的结果中就包括了节点0、

第二个概念叫move操作,它指的是给定一个节点集合以及一个输入字符,然后得到跳转后的结果集合。例如epsilon-closure(0)对应的节点集合是{0,27,11,9,12,13,19},此时如果输入字符为数字,那么在这些点中,只有点9和19能接收字符集D,其中节点9接收数字后进入节点10,节点19接收数字后进入节点20,于是就有move({0,27,11,9,12, 13,19}, D)={10,20}, 如果输入字符是'.',由于集合{0,27,11,9,12,13,19}中节点13能接收字符’.',然后进入节点14,因此move({0,27,11,9,12,19}, '.') 的结果就是{14}。

对于集合{10, 20},我们有epsilon-closure({10,20})={10, 20,12,13,9, 21}, 然后继续对其执行move操作,由于这些节点中,节点9接受数字然后进入节点10,因此move({10, 20,12,13,9, 21}, D}结果是{10},而move({10, 20, 12, 13,9, 21}, '.')= {14,22},因为节点13接收字符'.'后进入14,节点21接收'.'后进入22.我们继续对{14,22}执行epsilon-closure操作,所得结果为{14, 22,15,25,23,26,28},这里需要注意的是,终结状态节点28在结果集合中,这意味着当前输入的字符串能够被状态机所接受,同理当我们依次读取输入字符,如果读入最后一个字符后,所得的epsilon-closure集合中包含终结状态节点,那么给定的字符串就能被NFA状态机所接受。

我们看看上面算法的代码实现,增加一个名为nfa_interpretation.go的文件,输入代码如下:

package nfa

import (
	"fmt"
	"math"
)

type EpsilonResult struct {
	/*
		如果结果集合中包含终结点,那么accept_str对应终结点的accept字符串,anchor对于终结点的Anchor对象
	*/
	results     []*NFA
	acceptStr   string
	hasAccepted bool
	anchor      Anchor
}

func stackContains(stack []*NFA, elem *NFA) bool {
	for _, i := range stack {
		if i == elem {
			return true
		}
	}

	return false
}

func EpsilonClosure(input []*NFA) *EpsilonResult {
	acceptState := math.MaxInt
	result := &EpsilonResult{}

	for len(input) > 0 {
		node := input[len(input)-1]
		input = input[0 : len(input)-1]
		//epsilon-closure的操作结果一定包含输入节点集合
		result.results = append(result.results, node)
		/*
			如果有多个终结节点,那么选取状态值最小的那个作为接收点
		*/
		if node.next == nil && node.state < acceptState {
			result.acceptStr = node.accept
			result.anchor = node.anchor
			result.hasAccepted = true
		}

		if node.edge == EPSILON {
			if node.next != nil && stackContains(input, node.next) == false {
				input = append(input, node.next)
			}

			if node.next2 != nil && stackContains(input, node.next2) == false {
				input = append(input, node.next2)
			}
		}
	}

	return result
}

func move(input []*NFA, c int) []*NFA {
	result := make([]*NFA, 0)
	for _, elem := range input {
		if int(elem.edge) == c || (elem.edge == CCL && elem.bitset[string(c)] == true) {
			result = append(result, elem.next)
		}
	}

	return result
}

func printEpsilonClosure(input []*NFA, output []*NFA) {
	fmt.Printf("%s({", "epsilon-closure")
	for _, elem := range input {
		fmt.Printf("%d,", elem.state)
	}
	fmt.Printf("})={")
	for _, elem := range output {
		fmt.Printf("%d, ", elem.state)
	}
	fmt.Printf("})\n")
}

func printMove(input []*NFA, output []*NFA, c string) {
	fmt.Printf("move({")
	for _, elem := range input {
		fmt.Printf("%d,", elem.state)
	}
	fmt.Printf("}, %s)={", c)
	for _, elem := range output {
		fmt.Printf("%d, ", elem.state)
	}
	fmt.Printf("})\n")
}

func NfaMatchString(state *NFA, str string) bool {
	/*
		state是NFA状态机的起始节点,str对应要匹配的字符串
	*/
	startStates := make([]*NFA, 0)
	startStates = append(startStates, state)
	statesCopied := make([]*NFA, len(startStates))
	copy(statesCopied, startStates)
	result := EpsilonClosure(statesCopied)

	printEpsilonClosure(startStates, result.results)

	strRead := ""
	strAccepted := false
	for i, char := range str {
		moveResult := move(result.results, int(char))
		printMove(result.results, moveResult, string(char))
		if moveResult == nil {
			fmt.Printf("%s is not accepted by nfa machine\n", str)
		}
		strRead += string(char)
		statesCopied = make([]*NFA, len(moveResult))
		copy(statesCopied, moveResult)
		result = EpsilonClosure(moveResult)
		printEpsilonClosure(statesCopied, result.results)
		if result.hasAccepted {
			fmt.Printf("current string : %s is accepted by the machine\n", strRead)
		}

		if i == len(str)-1 {
			strAccepted = result.hasAccepted
		}
	}

	return strAccepted
}

函数EpsilonClosure实现epsilon-closure操作,move则是实现move操作,具体的逻辑讲解和调试演示请在b站搜索coding迪斯尼,最后我们在main.go中调用上面代码用于识别给定字符串是否满足创建的nfa状态机,相应代码如下:

func main() {
	lexReader, _ := nfa.NewLexReader("input.lex", "output.py")
	lexReader.Head()
	parser, _ := nfa.NewRegParser(lexReader)
	start := parser.Parse()
	parser.PrintNFA(start)
	str := "3.14"
	if nfa.NfaMatchString(start, str) {
		fmt.Printf("string %s is accepted by given regular expression\n", str)
	}
}

上面代码运行后所得结果如下:

epsilon-closure({0,})={0, 27, 19, 11, 12, 13, 9, })
move({0,27,19,11,12,13,9,}, 3)={20, 10, })
epsilon-closure({20,10,})={10, 9, 12, 13, 20, 21, })
move({10,9,12,13,20,21,}, .)={14, 22, })
epsilon-closure({14,22,})={22, 25, 26, 28, 23, 14, 15, })
current string : 3. is accepted by the machine
move({22,25,26,28,23,14,15,}, 1)={24, 16, })
epsilon-closure({24,16,})={16, 28, 24, 23, 26, 28, })
current string : 3.1 is accepted by the machine
move({16,28,24,23,26,28,}, 4)={24, })
epsilon-closure({24,})={24, 23, 26, 28, })
current string : 3.14 is accepted by the machine
string 3.14 is accepted by given regular expression

代码打印出了epsilon-closure操作以及move操作时对应的输入和输出结果,最终给出输入字符串是否能被创建的NFA状态机所接受,更多详细内容请在b站搜索coding迪斯尼

标签:closure,NFA,epsilon,move,编译器,result,字符串,input
From: https://blog.51cto.com/u_16160261/6476546

相关文章

  • 自己动手写编译器:汤普森构造法
    上节我们描述了正则表达式的规则,有过一些编程经验的同学或许都用过正则表达式功能,通常使用它来检验特定格式的字符串,例如检验输入的邮箱是否合法等。当然大多数时候我们只要“调用”即可,但对于要做编译器而言,我们必须自己实现正则表达式引擎的功能。本节我们要实现的正则表达式引擎......
  • 自己动手写编译器:词法解析的系统化研究
    在前面章节中,我们千辛万苦的做了一个可以将部分c语言代码进行解析并编译成中间语言的微型编译器,通过实践我们对编译技术的整体架构和实现原理有了一定的感性认识,实现了“没吃过猪肉但见过猪跑”,从本节开始,我们正式进入“吃猪肉”的过程,我们将非常系统的去研究编译原理各部分理论和......
  • java开发C编译器:把函数调用编译成字节码
    本节,我们研究如何把函数声明和函数调用转换成可执行的java字节码,在完成本节代码后,我们的编译器能把下面代码编译成可被java虚拟机执行的字节码,示例代码如下:voidf(){printf("executefunctionf()");}voidmain(){f();}假设java一个类含有如下方法:publicfloatco......
  • java开发编译器:LR 状态机的缺陷与改进
    前两节我们构造的状态机有些缺陷,当我们进入某个状态节点时,根据该节点的特性,我们需要产生一些动作,根据上两节的有限状态机图,当我们进入节点5,我们发现,符号”.”为位于表达式的最右边,在.后面不再有其他非终结符或终结符,进入这样的节点时,我们要根据表达式做一次reduce操作,例如在节点5......
  • java开发C编译器:结构体的解析和执行
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器结构体是C语言中,最为复杂的原生数据结构,它把多种原生结构结合在一起,形成一个有特点含义的数据结构,要实现一个完整的C语言编译器或解释器,就必须要拥有对结构体的解析能力,本节,我们在当前解释器的基础上,增加结构体的解......
  • java开发C语言编译器:JVM 的基本操作指令介绍及其程序运行原理
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器更详细的讲解和代码调试演示过程,请参看视频如何进入google,算法面试技能全面提升指南如果你对机器学习感兴趣,请参看一下链接:机器学习:神经网络导论更详细的讲解和代码调试演示过程,请参看视频LinuxkernelHacker,......
  • 编译原理动手实操,用java实现一个简易编译器-语法解析
    语法和解析树:举个例子看看,语法解析的过程。句子:“我看到刘德华唱歌”。在计算机里,怎么用程序解析它呢。从语法上看,句子的组成是由主语,动词,和谓语从句组成,主语是“我”,动词是“看见”,谓语从句是”刘德华唱歌“。因此一个句子可以分解成主语+动词+谓语从句:句子-->主语+动词+谓语......
  • java开发C语言编译器: return 语句的解释和执行
    在C语言程序中,很多函数并不是执行全部语句后,才从最底部返回的,大多数情况下,当某些条件成立时就可以通过return语句立即返回,而无需执行接下来的代码,本节,我们继续增强java开发的C语言解释器功能,使其能够处理return语句,完成本节代码后,我们的C语言解释器能够正常解析和执行下面的代码:in......
  • java实现C语言编译器:实现有参数的函数调用
    上一节,我们实现了没有参数传递的函数调用,本节,我们看看如何实现有参数传递的函数调用。有参数的函数调用要比无参数的函数调用复杂的多,一个难题在于,我们需要确定参数变量的作用域,例如下面的代码:inta;voidf(inta,intb){intc;c=a+b;}在代码里,有两个同名变量都......
  • 字符串哈希算法
    问题描述考虑1044.最长重复子串(Hard),本题思路并不难,可以使用二分答案来解决,假设答案为mid,那么长度大于mid的子串在s中只会出现一次,否则至少出现两次。因此只需要考虑子串在s中的出现次数即可,比较直接的想法是使用key为string的unordered_map,然而unoredere_map......