探索lexmachine
——Golang的顶级词法分析框架
由Tim Henderson创建,并于2014年至2017年发布,遵循BSD 3-Clause许可协议的lexmachine
是一个强大的词法分析框架,专为Golang打造。这个项目旨在成为Go语言中最佳、最快且最易用的词法分析解决方案。
什么是lexmachine
?
lexmachine
提供了一个有限的语言,支持用于复杂编程语言词法分析的受限但实用的正则表达式。其特色包括子词法器和非正规词法分析“逃生舱”功能,使得处理如嵌套C风格注释或其它成对结构的解析在词法分析阶段即可完成。
订阅邮件列表,获取重大更新、新版本发布以及重要补丁的通知。
目标
lexmachine
致力于成为Go语言的终极词法分析系统,无论你是新手还是经验丰富的开发者,都能轻松上手。
技术详解
- 文档链接:访问官方GoDoc获取详细API文档。
- 叙事性文档:了解项目背后的故事与设计理念。
lexmachine
中的正则表达式:掌握如何使用框架内的正则表达式进行匹配。- 历史:跟踪项目的演变历程。
- 完整示例:通过实际例子快速上手。
文档概览
- 教程:参阅Hackthology上的
lexmachine
写作指南。 - 工作原理:理解如何利用DFA(确定有限自动机)和NFA(非确定有限自动机)实现更高效的词法分析(见Hackthology)。
- 与goyacc的配合:如果你打算将
lexmachine
与标准Yacc实现(或其衍生品)一起使用,请务必阅读相关示例(GitHub)。 - GoDoc图标:点击GoDoc徽章直接查看API文档。
带来的组件
lexmachine
包含了以下核心组件:
- 限制集正则表达式解析器。
- 正则表达式的抽象语法树(AST)。
- 使用回溯代码生成器将AST编译为NFA机器码。
- 既支持DFA也支持NFA模拟的词法分析引擎。
- 包含词元起始和结束列、行号以及关联的标记名的匹配对象。
- 一个“逃生舱”,允许在匹配后消费任意数量的字节来处理非正规令牌。
定义词法器
词法器是一组模式(正则表达式),用于划分字符串并对其进行分类。在编译器设计中,这些细分的字符串称为“词法元素”,而类别称为“标记类型”或简称为“标记”。该过程有时被称为“分词”、“词法分析”或“词法化”。
创建词法器
lexer := lexmachine.NewLexer()
添加模式
假设我们需要一个词法器,仅识别一类:大小写均可的单词“wild”(例如:Wild, wild, WILD等)。对应的正则表达式是[Ww][Ii][Ll][Dd]
。添加模式的方法如下:
lexer.Add([]byte(`[Ww][Ii][Ll][Dd]`), func(s *lexmachine.Scanner, m *machines.Match) (interface{}, error) {
return 0, nil
})
Add
接收两个参数:模式和一个名为“词法动作”的回调函数,它使你能将底层的machines.Match
对象转化为程序所需的有意义的对象。
词法动作
定义一些标记类型和一个标记对象:
Tokens := []string{
"WILD",
"SPACE",
"BANG",
}
TokenIds := make(map[string]int)
for i, tok := range Tokens {
TokenIds[tok] = i
}
接着创建一个词法标记:
type Token struct {
TokenType int
Lexeme string
Match *machines.Match
}
然后编写一个帮助函数,从Match
和标记类型构建Token
对象:
func NewToken(tokenType string, m *machines.Match) *Token {
return &Token{
TokenType: TokenIds[tokenType],
Lexeme: string(m.Bytes),
Match: m,
}
}
现在我们可以为之前的模式创建词法动作:
lexer.Add([]byte(`[Ww][Ii][Ll][Dd]`), func(s *lexmachine.Scanner, m *machines.Match) (interface{}, error) {
return NewToken("WILD", m), nil
})
编写此类动作函数可能会变得繁琐,可以考虑创建一个辅助函数来生成动作:
func token(tokenType string) func(*lexmachine.Scanner, *machines.Match) (interface{}, error) {
return func(s *lexmachine.Scanner, m *machines.Match) (interface{}, error) {
return NewToken(tokenType, m), nil
}
}
然后简洁地为我们的三个标记添加模式:
lexer.Add([]byte(`[Ww][Ii][Ll][Dd]`), token("WILD"))
lexer.Add([]byte(` `), token("SPACE"))
lexer.Add([]byte(`!`), token("BANG"))
内置的标记类型
很多程序都使用类似的标记表示。lexmachine
提供了可选的Token
对象,你可以用它来替代自定义的标记类。
type Token struct {
Type int
Value interface{}
Lexeme []byte
TC int
StartLine int
StartColumn int
EndLine int
EndColumn int
}
如需从machines.Match
结构构建Token
,使用扫描器的帮助方法的示例:
func token(name string, tokenIds map[string]int) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
return s.Token(tokenIds[name], string(m.Bytes), m), nil
}
}
添加多个模式
在构造复杂的计算机语言的词法器时,经常会出现多个模式能匹配相同字符串的情况。在这种情况下,词法器会遵循以下两个规则选择要匹配的模式:
- 优先选择能匹配未匹配文本最长前缀的模式。
- 如果出现平局,则选择在用户提供的列表中较早出现的模式。
例如,在编写Python词法器时,关键字如"class"和"def"可能与标识符的模式[A-Za-z_][A-Za-z0-9_]*
匹配。如果词法器定义为:
lexer.Add([]byte(`[A-Za-z_][A-Za-z0-9_]*`), token("ID"))
lexer.Add([]byte(`class`), token("CLASS"))
lexer.Add([]byte(`def`), token("DEF"))
那么关键字"class"和"def"就永远不会被找到,因为"ID"标记总会优先匹配。正确的做法应该是将关键词放在前面:
lexer.Add([]byte(`class`), token("CLASS"))
lexer.Add([]byte(`def`), token("DEF"))
lexer.Add([]byte(`[A-Za-z_][A-Za-z0-9_]*`), token("ID"))
跳过模式
有时我们希望对某些模式不产生标记,而是直接跳过,例如空格和注释。只需让动作函数返回nil, nil
:
lexer.Add(
[]byte("( |\t|\n)"),
func(scan *Scanner, match *machines.Match) (interface{}, error) {
// 跳过空格
return nil, nil
},
)
lexer.Add(
[]byte("//[^\n]*\n"),
func(scan *Scanner, match *machines.Match) (interface{}, error) {
// 跳过注释
return nil, nil
},
)
编译词法器
lexmachine
利用了有限状态机理论来高效地进行文本分词。词法器在使用之前,需要将其转换为非确定有限自动机(NFA)或确定有限自动机(DFA)。
构建时间是指将一组正则表达式转换为状态机所需的时间。对于NFA,它是O(r),其中r是正则表达式的长度;而对于DFA,理论上最坏情况可能是O(2^r),但在实践中通常不会超过O(r^3)。此外,lexmachine
的DFA还会自动最小化,以减少内存占用。
总结
lexmachine
不仅仅是一个普通的词法分析工具,它提供了强大且灵活的接口,使得在处理复杂语言时能得心应手。无论是小型项目还是大型工程,它都是值得信赖的选择。立即加入lexmachine
的社区,探索更多可能性,享受编程的乐趣!