实现中文编程语言:解析器上的明珠使用现代的PEGs

举报
码乐 发表于 2024/02/26 10:56:45 2024/02/26
【摘要】 4.0 解析如何工作: PEGs 解析词令不使用EBNF,有更现代的PEGs。而EBNF的替代者为 PEGs 结束符: assignment <- NAME '=' expr ';' 运算符: expr <- term { '+'/'-' term } 乘除: term <- factor { '*'/'/' factor } 词: factor <- NUMBER / NAME / ...

4.0 解析如何工作: PEGs 解析词令

不使用EBNF,有更现代的PEGs。

而EBNF的替代者为 PEGs

	结束符:  assignment <- NAME '=' expr ';'
	运算符: expr <- term { '+'/'-' term }
	乘除: term <- factor { '*'/'/' factor }
	词:  factor <- NUMBER / NAME / '(' expr ')'

PEGs 看起来类似EBNF,选择 / 取代了 |, 比如:

	rule <- e1 / e2 / e3

它的解析规则是:

	尝试 解析 e1, 如果成功,完成
	或者,解析 e2,如果成功,完成
	或者,解析 e3,如果成功,完成
	或者,报错。

它的规则秩序意义在于: 规则列表的第一个具有较高优先级。 这意味着更好的重试错误跟踪回溯。
PEGs是更现代的,解析表达式语法大多数编译器书籍并没有它的介绍.
相关材料: 基于识别的语法基础。 POPL 2004 ACM

4.1 错误处理:消费需要前瞻性,一个前瞻点 LookHead

如何观察现在是否匹配,我们把词令转换为模型,并以前面所讲的解析方式PEGs解析为语句
我们先观察Tokens的Data 如何被准备以消费的。
首先准备结束标记

	type EOFs struct {
		Type   string
		Value  string
		Lineno string
	}

然后准备控制方式,包括从链表获取token,如果不知道如何做,可以参考我前面的系列文章

	///Data 来自分词器的传入令牌, 可迭代对象
	type Tokens struct {
		Data      *tlink
		Lookahead *Token
	}

	// 获取一个Token, FIFO 模式返回链表的 单个 Token 对象 不更新 当前 that 的属性,
	func (that *Tokens) GetToken() (*Token, *EOFs) {
		...
	}

在Tokens管理对象中,刷新获取到的token对象到 前瞻点

    //刷新 token 到 lookhead
    // expected_types可选参数, 刷新 下一个 token
	// 当 Lookahead 的 类型与 expected_types 中的类型一致时,返回 Lookahead
	func (that *Tokens) Peek(expected_types ...string) *Token {
		
		if that.Lookahead == nil { 
			tok, eof := that.GetLinkToken()
			if eof != nil {
				return nil
			}
			that.Lookahead = tok
		}
		if expected_types != nil {
			for _, st := range expected_types {
				if that.Lookahead.Type == st {
					return that.Lookahead
				} else {
					continue
				}
			}
		} else {
			return nil
		}
		return nil
	}

在此时,Peek函数将作为主要的裁定是否符合前文提到的解析规则,并在此处控制是否匹配并返回。

4.2 把token转化为 有意义的语句,以便执行

接3.2,从Add开始,我们实现前文提到的解析顺序

	//后加减
	func ParseAddTerm(tokens *Tokens) *Node {
		var left *Node
		left = ParseMulTerm(tokens)
		for {
			tok, bl := tokens.Accept([]string{"PLUS", "MINUS"}...)
			if !bl {
				break
			}
			right := ParseMulTerm(tokens)
			Nodes := &Node{Data: &BinOp{Op: tok.Value, Left: left, Right: right}}
			left = Record_lineno(Nodes, tok.LineNo)
		}
		return left
	}

让我们重复一次这个函数的逻辑:解析词:

	term:
    	  start -> factor -> -end->
           |			|
    	     <-     *       <-	
    	   |			|
    	     <-     /       <-	

	//第二乘除
	func ParseMulTerm(tokens *Tokens) *Node {
		var left *Node
		left = ParseLgTerm(tokens)

		for {
			tok, bl := tokens.Accept([]string{"TIMES", "DIVIDE"}...)
			if !bl {
				break
			}
			right := ParseLgTerm(tokens)
			Nodes := &Node{Data: &BinOp{Op: tok.Value, Left: left, Right: right}}
			left = Record_lineno(Nodes, tok.LineNo)
		}
		return left
	}

	//第一
	func ParseLgTerm(tokens *Tokens) *Node {
		var left *Node
		left = parse_factor(tokens)

		for {
			tok, bl := tokens.Accept([]string{"LAND", "LOR"}...)
			if !bl {
				break
			}
			right := parse_factor(tokens)
			Nodes := &Node{Data: &BinOp{Op: tok.Value, Left: left, Right: right}}
			left = Record_lineno(Nodes, tok.LineNo)
		}
		return left
	}

解析因子 factor:

    	   start -> INTEGER -> -end->
    	   |			|
    	      <-    FLOAT      <-	
    	   |			|
    	      <-    NAME       <-	

	//term等
	func parse_factor(tokens *Tokens) *Node {
		// 运算符解析
		if optok, ok := tokens.Accept("INTEGER"); ok {
			intger := Integer{Value: optok.Value}
			nodes := MakeNode(intger)
			modelRef := Record_lineno(nodes, optok.LineNo)
			return modelRef
			//正负号
		} else if optok, ok := tokens.Accept([]string{"PLUS", "MINUS"}...); ok {
			values := parse_factor(tokens)
			Nodes := &Node{Data: &UnaryOp{Op: optok.Value, Operand: values}}
			return Record_lineno(Nodes, optok.LineNo)
		} else {
			msg := fmt.Sprintf("Bad factor with tokens:%v \n", tokens)
			logger.Panic(msg) 
		}
		return nil
	}

学习,在实践中求证,在思考中提炼。大难不死,必有后福,巨大的痛苦,同时其后的乐趣也是丰厚的。
我们将得到一些回报。

4.3 语法的最后,语句

解析表达式_Parsing Expressions 语句: 把指令集成为语句,statement。

我们已经了解了PEGs本质上很类似于代数的运算顺序,并且有一个形象的卷的过程。现在让我们回顾,我们在干什么

	1 理解如何指定语法
	2 消费:提炼一种如何解析计算任务的直觉
	3 组合为语句

正常的消耗令牌

    //正常消费预期匹配的词令牌,否则返回nil 不消耗
    func (that *Tokens) Accept(expected_types ...string) (*Token, bool) {

            tok := that.Peek(expected_types...)
            if tok != nil {
                    that.Lookahead = nil
                    return tok, true
            }

            return tok, false
    }

比如实际的比如下代码

	print 3 + 4 * 5;

我们将得到一个结果:

	model *tlink {size: 1, 
		head: *nodes (*Node)(0xc0001011d0)
			{data:BinOp {Expression: nil, 
				Op: "+", 
				Left: *Node {Next: nil, 
					Data: (*Integer) 
					data:{Value:"3"}, 
				Right: *Node {Next: nil, 
					Data: (*BinOp) BinOp {Expression:  nil, 
						Op: "*", 
						Left: *Node {Next: nil, 
							Data: (*Integer),
							data:{Value:"4"}, 
						Right: *Node {Next: nil, 
							Data: (*Integer),
							data: {Value:"5" }
				prev: *nodes nil, 
				next: *nodes nil}, 
		tail: *nodes (*Node)(0xc0001011d0)
			{data: ...}

这正是我们需要的

现在我们需要实现对tokens链的消费,比如我们需要把如下表达式实现

	3 + 4 * 5; 

其计算顺序是

	(3) + (4 * 5)

tree-walk 表示

             +            --->        +             ---> 23
	  *    3                  20     3
	4   5

在下一章节我们开始在硬件中组织计算。

5 结语:

选择发布在这个平台,是希望借华为平台这个巨人的路径走的更远。 谢谢。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。