每一种动物和鸟类都有自己的声音和特征,有时选择使用某些奇特的乐器,可以让声音听起来更接近他们。
__hans zimmer 著名当代音乐艺术家
接上: 写在前面
语法, 人们知道如何控制. 前面我们已经将原始源代码作为一个字符串,并将其转换为稍微更高级别的表示:一系列Tokens。在这一章编写的解析器获取这些词令,并再次将它们转换为更丰富、更复杂的表示。
每一个成功的通用语言,都由众多内置成功的“小语言”构成,它们无处不在,或许有一千个。但行话为了区别和不那么烂大街。有了“特定领域语言”的名称。这些是为特定任务量身定制的。 比如 应用程序脚本语言、模板引擎、标记格式和甚至是配置文件语言。
在这一章,我们进入第一个重要里程碑,我们将实现获取文本并将每个单词映射到语言的语法。我们在这里以同样的方式使用解析器,只是我们的语言比古法自然语言更现代一些。
3.1 面对问题 表示代码: 语句 从词令到代码语句
这里我们将要解决的问题是如何把 token 链转换为数据模型。
Tokens -> Data model (AST)
让我们专注于主要目标——代码语句的表示。我们已经写过分词器,应该很简单。如果没有用过解释器,也没有关系。也许你的直觉会有所帮助。 我们在中学就已经知道旧的人类内容 关于运算顺序:
+ - x ÷
当他们同时存在一个计算式中的时候,
我们总是需要先计算 乘法和除法,如果有更高优先级的如 位和 () 则更优先他它们,
这里我们先讨论基本的 逻辑运算和算术运算。
我们知道乘法在加法或减法前计算。当我们使用树时。叶节点是数字,内部节点是操作符,每个操作数都有分支,比如从叶子到根的一个后序遍历。
A. 从完整的树开始,评估最底部的操作,2 * 3
B. 现在我们可以执行+.
C. 接下来,-.
D. 最终答案。
# 注释: 乔姆斯基层次结构
我们打算使用模型的方式来确定它们到底如何计算。就像前面的分词器的词法文法一样,围绕代码语句文法有大量的理论。我们将比在分词器的扫描时稍微更多了解它们,因为它是贯穿大部分解释器和语言的有用逻辑。
并且我们将讨论一种称为 Parsing Expression Grammar PEGs, 也就是解析表达式语法的方式,
(PEG)
assignment <- NAME '=' expr ';'
expr <- term { '+'/'-' term }
term <- factor { '*'/'/' factor }
factor <- NUMBER / NAME / '(' expr ')'
因此,我们需要执行以下几步
1 理解如何指定语法
2 提炼一种如何解析计算任务的直觉
3 写一个自己的解析器
3.2 解析器 语句 Representing_Code: 让它把摄入的词令转换为语句的一部分
我们需要支持 逻辑 与或 和 计算器 加减乘除,我们约定以下运算符
Tokens -> Data model (AST)
首先规定语法规范
start -> location -> = -> expression -> end
其表达式解析顺序 如: term + term * term
expression:
start -> term -> -end->
| |
<- + <-
| |
<- - <-
| |
<- * <-
| |
<- / <-
词解析 如: 12
term:
start -> INTEGER -> -end->
| |
<- FLOAT <-
| |
<- NAME <-
代码如下:
//表达式
func ParseExpression(tokens *Tokens) *Node {
return ParseAddTerm(tokens)
}
至此,我们可以将Tokens处理为一个个符合人类计算顺序习惯的语句,并最终在计算机硬件中执行。 比如VM,LLVM等。
4 解析的控制 EBNF: 工具 把材料消费为token, 转换为一个一个的指令 token
现在我们需要实现对tokens链的消费,比如我们需要把如下表达式实现
2 + 3 * 4 * 5 - 6 * 7 - 8;
其计算顺序应该是
(2) + (3 * 4 * 5) - (6 * 7) - (8)
其计算顺序为表达式 :
start -> term -> -end->
| |
<- + <-
| |
<- - <-
解析因子 :
start -> INTEGER -> -end->
| |
<- FLOAT <-
| |
<- NAME <-
解析词:
term:
start -> factor -> -end->
| |
<- * <-
| |
<- / <-
如此每个层次的词令描述,低优先的运算以更高优先级链接。 符号扫描被称为一种 BNF(Backus Naur Form) 表单方式进行。 左部的任何符号都可以被右部的符号替换,等效的,
x = y + 22
z = x - 11
替换 x
z = (y + 22) - 11
另有一种EBNF的类似方式,比BNF更复杂的计算表达式解析方式,更接近数学的计算解析,这里不再深入。
expr = term { '+'|'-' term }
term = factor { '*'|'/' factor }
factor = INTEGER | FLOAT | NAME | '(' expr ')'
EBNF 是一种非常普遍的语法规范,有大量文档可查看,在通用语言中也有应用。
4.0 解析如何工作: 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 让我们回顾,我们在干什么
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
在下一章节我们开始在硬件中组织计算。
结语 二:
选择发布在掘金这个平台,是希望借掘金平台这个巨人的路径走的更远。 谢谢。