实现中文编程语言:分词令牌
1 简介
这一节,我们继续挑战中文编程语言的旅程。
2 词素与标记
我们已经在上一节有了一个获得代码的方式,安装之前的约定,需要分别按以下词素去分解代码。
我们规定如下:
中文 英文 运算符
刷 print PRINTI /PRINTF
与 LAND &&
或 LOR ||
加 PLUS +
减 MINUS -
乘 TIMES *
除 DIVIDE /
完 END ;
为了方便,我们把它分类别存起来
var (
_liter_tokens = map[string]string{
";": "SEMI",
}
_operate_tokens = map[string]string{
"&&": "LAND", // Logical and
"||": "LOR", // Logical or
"+": "PLUS",
"-": "MINUS",
"*": "TIMES",
"/": "DIVIDE",
}
_keys_words = map[string]string{
"int": "INTEGER",
"float": "FLOAT",
"string": "STRING",
"print": "PRINT",
}
_zh_keys = map[string]string{
"刷": "PRINT",
"加": "PLUS",
"减": "MINUS",
"乘": "TIMES",
"除": "DIVIDE",
"与": "LAND",
"或": "LOR",
}
_zh_ops = map[string]string{
"PRINT": "print",
"PLUS": "+",
"MINUS": "-",
"TIMES": "*",
"DIVIDE": "/",
"LAND": "&&",
"LOR": "||",
}
)
我们将要实现一个 tree-walk的语言,然而词素只是源代码的原始子串。
然而,在将字符序列分组为词素的过程中,我们还偶然发现了一些其他有用的信息。当我们将词素与其他数据捆绑在一起时,结果是一个语句。它包括有用的东西。
然而,在将字符序列分组为词素的过程中,我们还偶然发现了一些其他有用的信息。
当我们将词素与其他数据捆绑在一起时,结果是一个语句。它包括有用的东西。
//model
type Node struct {
Next *Node
Id string
Data any
}
type Integer struct {
// Example: 4
Value string
}
//二元运算
type BinOp struct {
// Example: left + right 运算
Op string
Left *Node
Right *Node
}
//一元运算
type UnaryOp struct {
// Example: -operand
Op string
Operand any
}
//刷
type PrintStatement struct {
// print value;
Value any //Expression
}
现在我们定制了一些模型结构体,以使得这些代码可以组织为对应的 Token并在语句识别时,按语法 组织为语句。
2.2 分词器中的中文:词素令牌类型 Token type
关键字是语言语法形状的一部分,因此解析器通常有这样的代码,
“如果下一个标记是while for . . . ”这意味着解析器不仅想知道它有某个标识符的词素,还想知道它有一个保留字,以及它是哪个关键字。
但是篇幅所限,我们不打算实现这样的东西,按约定,该分词器可以通过比较字符串分类从原始语义中分拣,但这是缓慢的,那很难看。
从我们认识语义的角度,我们还记得哪一种语义的它代表。我们为每个关键字、运算符、标点符号和文字类型 都定义不同的类型。
有文字值的词素——数字和字符串等等。
由于扫描器必须遍历字面量中的每个字符才能正确识别它,因此它还可以将值的文本表示转换为解释器稍后将使用的实时运行时对象。
首先,我们约定一个错误处理方式,这个好像在学校中是不可谈及的话题,我们在这里简约处理,当不匹配我们的规则时,只是显示警告:
func ErrorHander(lineno int, errinfo string) {
fmt.Printf("WARNING:%v %v", lineno, errinfo)
}
我们分词后可以将其存入BST树之类的结构中,以便使用梯度下降类似的方式去解析Token,这里我们使用tree-walk的方式,如果对于结构有所疑问,可以参考之前的文章。
在循环的每一轮中,我们扫描一个令牌。这是分词器的真正核心。我们将从简单的开始。想象一下,如果每个词素只有一个字符长。您需要做的就是使用下一个字符并为其选择和记录类型。
/*
解析中文,数字,符号
*/
func TokenizeZhs(q string) *tlink {
// 文本流队列入口
var lineno int = 1
var linkedq = makeDlink()
var strInt string = ""
for n, s := range q {
s := s
if string(s) == " " {
if strInt != "" {
GenTokenLinked("int", string(strInt), linkedq, lineno)
}
strInt = ""
continue
} else if string(s) == "\t" {
continue
} else if string(s) == "\r" || string(s) == "\n" {
lineno += 1
continue
} else if unicode.IsDigit(s) {
strInt = strings.Join([]string{strInt, string(s)}, "")
continue
//结束一个语句 ;
} else if string(s) == ";" {
if strInt != "" {
GenTokenLinked("int", string(strInt), linkedq, lineno)
}
strInt = ""
GenTokenLinked(";", ";", linkedq, lineno)
continue
//处理分词中文
} else if unicode.IsLetter(s) {
allLetter := string(s)
if _zh_keys[string(allLetter)] != "" {
tokType := _zh_keys[string(allLetter)]
GenTokenLinked(_zh_ops[tokType], tokType, linkedq, lineno)
} else {
GenTokenLinked("NAME", allLetter, linkedq, lineno)
}
continue
} else {
errsInfo := fmt.Sprintf("Unterminated character %v \n", string(q[n]))
ErrorHander(lineno, errsInfo)
}
logger.Printf("end all at:%v \n", n)
}
// 填充结束 标志
GenTokenLinked("END", "END", linkedq, lineno)
return linkedq
}
这就是把中文词素加载到我们的token令牌中的关键一步,它看起来非常简单,性能不是我们的主要关注点,正好满足我们的简单约定。
2.2.1 解读: ZH令牌,数字类型,运算符,操作符,结束符
我们如何知道一个字符是否可以称为 词令牌,以便我们完成源码到模型的过程: source -> model
当我们遇到这样的语句: 刷 11 加 23;
字符流(从源码获得):
("刷"," ",1","1"," ", "加", " ", "2", "3", ";")
我们需要把它变成tokens:
[('PRINT', 'print'), ('INTEGER', '11'), ('PLUS', '+'), ('INTEGER', '23'), ('SEMI', ';')]
中文词素,我们按字符读取了字符串中的内容,并且将其与我们的词素进行了 确认,符合要求的则添加到我们的令牌链表
} else if unicode.IsLetter(s) {
allLetter := string(s)
if _zh_keys[string(allLetter)] != "" {
tokType := _zh_keys[string(allLetter)]
GenTokenLinked(_zh_ops[tokType], tokType, linkedq, lineno)
} else {
GenTokenLinked("NAME", allLetter, linkedq, lineno)
}
continue
同样的处理方式,我们分辨数字类型并将其最终封装为令牌,存入链表以待处理
当一个数字类型的字符不只一个时,我们需要在下一次循环中一并添加和处理,并将指针置为空。
if strInt != "" {
GenTokenLinked("int", string(strInt), linkedq, lineno)
}
strInt = ""
} else if unicode.IsDigit(s) {
strInt = strings.Join([]string{strInt, string(s)}, "")
continue
}
扫描下一个字符并找出它的 TokenType,然后添加到 tokens 链表中。 最后完全返回。
同时,我们需要一些辅助方法,将我们识别到的 类型存入和打包到链表。
/*
@param: keys token类型
@param: val token 值
@param: t 将要存入的结构
@param: lineno 行号记录
// 将 keys val 存入 t dlist中, 记录lineno
*/
func GenTokenLinked(keys, val string, t *tlink, lineno int) bool {
// 直接判断属于那个 token 关键字 并填充到 token队列
if keys == "END" {
PutinTokenLinked("END", "END", t, lineno)
return false
} else if _liter_tokens[keys] != "" {
keysType := _liter_tokens[keys]
PutinTokenLinked(keysType, val, t, lineno)
return true
} else if _operate_tokens[keys] != "" {
keysType := _operate_tokens[keys]
PutinTokenLinked(keysType, val, t, lineno)
return true
} else if _keys_words[keys] != "" {
keysType := _keys_words[keys]
PutinTokenLinked(keysType, val, t, lineno)
return true
} else {
PutinTokenLinked("NAME", val, t, lineno)
return false
}
}
func PutinTokenLinked(types, val string, t *tlink, lineno int) *tlink {
TokenNew := &Token{Type: types, Value: val, LineNo: fmt.Sprintf("%v", lineno)}
t.AppendToken(TokenNew)
return t
}
它们获取当前词素的文本并为其创建一个新Token令牌。我们将很快使用另一个方法来处理具有价值的令牌。
如果全部约定的 词素都不能与文本匹配,则交由ErrorHandler处理
else {
errsInfo := fmt.Sprintf("Unterminated character %v \n", string(q[n]))
ErrorHander(lineno, errsInfo)
}
另外,我们使用简单的 键值对 匹配来确认扫描到的字符是否是提取约定的字符。
} else if _keys_words[keys] != "" {
...
3 写在最后
最后,关于语句结束符 ;分号几乎每一种新语言都会刮掉的一点句法;(以及一些像 BASIC 这样的古老语言从未有过)。他们将换行符视为语句终止符,这样做是有意义的。“有意义的地方”部分是具有挑战性的部分。
然而我们希望 仍然把分号;作为显式语句终止符。
虽然大多数 语句都在它们自己的行上,但有时您需要将单个语句分布在几行中。那时混合的换行符不应被视为终止符。
大多数应该忽略换行符的明显情况很容易检测到,但也有一些意外的情况。
虽然程序员和其他人一样是时代的产物,分号是和所有大写关键字一样的传统。
只是要确保您选择了一组对您的语言的特定语法和习语有意义的规则。
- 点赞
- 收藏
- 关注作者
评论(0)