tpl

package
v1.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 17, 2020 License: Apache-2.0 Imports: 8 Imported by: 5

README

tpl

TPL 全称是 Text Processing Language(文本处理语言)。整个库的组成如下:

  • github.com/qiniu/text/tpl: 基础的 TPL 文本处理引擎,外加一个 TPL 文法编译器。
  • github.com/qiniu/text/tpl/interpreter: 在 TPL 基础上实现的解释器引擎。典型使用场景是实现一个计算器(Calculator)、解释型的语言执行器(直接产生执行结果)或翻译器(翻译成另一种语言,比如字节码)。
  • github.com/qiniu/text/tpl/{number, cmplx, rat}: 在解释器框架基础上实现的3个计算器,分别对应三种类型的运算:float64(浮点数)、complex128(复数)、*big.Rat(有理数)。
  • github.com/qiniu/text/tpl/exmples: TPL 库的一些样例。

文本处理引擎

TPL 把文本处理分为了两个阶段。

第一个阶段是词法分析,由 Tokenizer(也叫 Scanner)完成。Tokenizer/Scanner 将文本流转换为 Token 序列。简单来说,就是一个 text []byte => tokens []Token 的过程。尽管世上语言多样,但是词法非常接近,所以这块 TPL 只是抽象了一个 Tokenizer 接口,方便用户自定义。TPL 也内置了一个与 Go 语言词法完全类似的 Scanner(做了一个非常细微的调整,就是增加了 '?'、'~'、'@' 等操作符)。

第二个阶段是语法分析。由于 Go 语言不支持操作符重载,语法表示我们用文本来表达,通过 TPL 文法编译器编译成语法 DOM 树。当前我们支持的 TPL 文法如下:

基本token

基本token的表示方式又分为以下几类:

  • TOKEN。通常以全大些的字母表达。如:IDENT、INT、FLOAT、IMAG、STRING、CHAR、COMMENT 等。
  • 单字符操作符。如:'+'、'-'、'*'、'/' 等等。这些其实也可以用上面 TOKEN 形式表达,只是不那么直观。如 '+' 可以用 ADD,'-' 可以用 SUB 等等。
  • 多字符操作符。如: "++"、"--"、"+="、"<=" 等等。这些其实也可以用上面 TOKEN 形式表达,只是不那么直观。"++" 可以用 INC,"+=" 可以用 ADD_ASSIGN 等等。
  • 关键字。如:"if"、"return"、"class" 等等。TPL 内建的 AutoKwScanner 可以自动识别语言的关键字。其原理是,只要在 TPL 文法中出现了满足 '"' IDENT '"' 形式的规则,那么就认为里面 IDENT 就是一个关键字。
特殊token
  • EOF:即 tpl 中的 GrEOF 规则。不匹配任何内容,但是可用于判断当前是否已经匹配完整个 token 序列。
  • 1:即 tpl 中的 GrTrue 规则。不匹配任何内容,无论如何永远匹配成功。
复合规则
  • *G: 反复匹配规则 G,直到无法成功匹配为止。
  • +G: 反复匹配规则 G,直到无法成功匹配为止。要求至少匹配成功1次。
  • ?G: 匹配规则 G 1次或0次。
  • @G: 要求其后的文本满足规则 G。如果要匹配的文本满足规则 G,则匹配成功,但是不真匹配任何内容(相当于只是 peek 下后续的 token 序列,要匹配的文本的当前匹配位置不前进)。例如:@'{' 这样一段规则表示的含义是要求要匹配的后续文本应该以 { 开头。
  • ~G: 要求其后的文本不满足规则 G。如果要匹配的文本满足规则 G,则匹配失败;如果不满足 G,则匹配成功,但不匹配任何内容(相当于只是 peek 下后续的 token 序列,要匹配的文本的当前匹配位置不前进)。例如: ~'{' 这样一段规则表示的含义是要求要匹配的后续文本不能以 { 开头。
  • G1 G2 ... Gn: 要求要匹配的文本满足规则序列 G1 G2 ... Gn。例如:"fn" '(' ')' 表示要匹配的文本去除所有空白和注释后是 fn() 这样的文本。
  • G1 G2! ... Gn: 可以在 G1 G2 ... Gn 中间的任何地方插入 ! 操纵符。它表示的意思是不可回退,或者说快速失败(fail fast)。例如 G1 G2! ... Gn 表达的含义是,如果 G1 G2 匹配成功了,那么后续的 G3 ... Gn 必须匹配成功,如果不成功则直接结束整个匹配报告失败。善用 ! 操作符可以改善错误提示信息,因为通常这时候提示的错误更为准确。
  • G1 | G2 | ... | Gn 要求要匹配的文本满足规则 G1 G2 ... Gn 中的任意一个。
  • G1 % G2: 列表运算。从规则上来说等价于 G1 *(G2 G1)。比如 IDENT % ',' 可以匹配 abc, abc, defg, fhi 这样的文本。
  • G1 %= G2: 可选列表运算。从规则上来说等价于 ?(G1 % G2)。和列表运算唯一差别是允许匹配的内容为空。
  • (G): 从规则上来说就是 G,只是改变运算的次序。详细见下“运算符优先级”。

运算符按优先次序,从高到低排序如下:

  • (G)
  • *G, +G, ?G, @G, ~G: 这些操作遵循右结合律,在最右边的运算优先。如:~+G 表示 ~(+G)
  • G1 % G2, G1 %= G2
  • G1 G2! ... Gn
  • G1 | G2 | ... | Gn
动作和标记

动作(action) 是指规则匹配成功的情况下执行的代码。如下:

tpl.Action(G, func(tokens []tpl.Token, g tpl.Grammar) {
	...
})

这里的 func 就是动作(action),整个表达式得到一个带动作的规则,使用上和一般的规则无异。

但在 TPL 文法毕竟无法内嵌 Go 语言代码,我们并无法直接内嵌一个动作(action)进去,取而代之的是标记(mark)。

标记的文法是这样的:

G/mark

这里 G 代表一个规则,而 mark 是一个合法的符号(IDENT)。在 TPL 文法中遇到标记(mark)时,TPL 编译器会产生一个回调(Marker),交给业务方来处理这个标记。如下:

Marker := func(g tpl.Grammar, mark string) tpl.Grammar {
	...
}

Maker 概念上并不是动作。但是你可以在 Maker 回调中生成相应的动作,并且通常你都在这样做。所以一般我们在沟通惯例上,会简单把标记(mark)和执行动作(action)等同起来。

通过定制 Marker,业务方就可以完成自己期望的业务逻辑。我们也有一些内建的 Marker 实现,进一步简化大家的文本处理过程。例如,我们接下来要介绍的“解释器(interpreter)”。

使用TPL

使用 TPL 的范式如下:

import (
	"github.com/qiniu/text/tpl"
)

// 定义要处理的文本内容对应的TPL文法
//
const grammar = `
	...
`

func eval(text []byte, fname string) (..., err error) {

	defer func() {
		if e := recover(); e != nil {
			// 错误处理
			return
		}
	}()

	marker := func(g tpl.Grammar, mark string) tpl.Grammar {
		...
	}
	compiler := &tpl.Compiler{
		Grammar: grammar,
		Marker:  marker,
	}
	m, err := compiler.Cl()
	if err != nil {
		// 错误处理
		return
	}
	err = m.MatchExactly(texr, fname)
	if err != nil {
		// 错误处理
		return
	}

	// ...
	return
}

解释器(interpreter)

解析器引擎(interpreter engine)在文本处理的模型上做出了更多的假设。它假设业务方实现如下接口:

type Stack interface {
	PopArgs(arity int) (args []reflect.Value, ok bool)
	PushRet(ret []reflect.Value) error
}

type Interface interface {
	Grammar() string
	Fntable() map[string]interface{}
	Stack() Stack
}

也就是说:

  • 要有一个栈(stack);
  • 要有一个函数表(fntable);

在 解释器(interpreter) 的 TPL 文法中,标记(mark)分为如下这些情况:

_mute

_mute 是一个内建的标记(mark)。顾名思义,它有禁止发言(禁止执行动作)的意思。展开来说:

  • 在第一次遇到 _mute 时,会禁用后续普通 mark 的执行,但 '_' 开头的 mark 不受影响。
  • 后续如果再遇到 _mute 时,mute 引用计数++,当 mute 计数 > 1 时,所有非内建的 mark 都会禁止执行(包括 '_' 开头的)。
_unmute

_unmute 是一个内建的标记(mark)。它是 _mute 的反操作,执行 mute 引用计数-- 的行为。当 mute 计数减少到 0 时,所有 mark 回到正常执行状态。

_tr

_tr 是一个内建的标记(mark)。它是一个调试用的标记,它在规则匹配成功时,将规则所匹配的文本打印出来。

用户定义标记

解释器(interpreter) 在遇到用户定义标记时,会查找 fntable 得到对应的函数。例如,假设用户自定义标记叫 add,那么我们会到 fntable 中查找名为 $add 的函数。如果找到,则:

先通过反射查看函数的第一个参数。这分为 3 种情况:

  1. 第一个参数是 interpreter.Stack 类型。会自动传入 interpreter 的 stack 实例。

  2. 第一个参数是 interpreter.Interface 类型。会自动传入用户实现的 interpreter 实例。

  3. 第一个参数是其他类型。

对于情形1和2,我们要求函数的参数只能是1个或2个,返回值要么没有,要么error类型。对于函数只有1个参数的情形,我们传入 stack 或 interpreter 实例然后调用之;对于函数有2个参数的情形,我们依据参数的类型分为如下几种情况:

  • 参数为 interface{} 类型。这表示这个函数希望传入规则匹配到的 tokens 序列(tokens []tpl.Token)。
  • 参数为 interpreter.Engine 类型。这表示这个函数希望传入解释器引擎(interpreter engine)。
  • 参数为其他类型,这时也有两种情形。一种是 mark 以大写字母开头(或者以 _ + 大写字母开头开头),则表示函数接受的是 Grammar.Len(),通常是当规则为 *G、+G、?G、G1 % G2、G1 %= G2 时告知你准确的成功匹配次数,此时函数第二个参数必须是 int 类型。另一种情况是小写开头(或者以 _ + 小写字母开头),表示函数希望传入规则匹配到的 tokens 序列的第一个,也就是 tokens[0] 对应的值(可能会依据类型的不同进行自动的类型转换,比如如果参数为 float64,那么我们会自动调用 strconv.ParseFloat 完成转换)。

对于情形3,我们自动根据所需的参数个数,从 stack 中弹出参数列表(通过调用 PopArgs),然后调用该函数,把返回的结果压回 stack(通过调用 PushRet)。

使用解释器

使用 interpreter 的范式如下:

第一步,先实作一个 interpreter 包(假设叫 foo,我们的样例 github.com/qiniu/text/tpl/{number, cmplx, rat} 都属于这一类):

package foo

import (
	"reflect"
	"github.com/qiniu/text/tpl/interpreter.util"
)

// 定义要处理的文本内容对应的TPL文法
//
const grammar = `
	...
`

// ----------------------------------------------------

type Stack struct {

}

func (p *Stack) PopArgs(arity int) (args []reflect.Value, ok bool) {
	...
}

func (p *Stack) PushRet(ret []reflect.Value) error {
	...
}

// ----------------------------------------------------

type Calculator struct {
	stk *Stack
}

func (p *Calculator) Grammar() string {
	return grammar
}

func (p *Calculator) Stack() interpreter.Stack {
	return p.stk
}

func (p *Calculator) Fntable() map[string]interface{} {
	return fntable
}

func init() {
	fntable = Fntable
}

var fntable map[string]interface{}

// ----------------------------------------------------

var Fntable = map[string]interface{}{
	...
}

然后引用这个 interpreter 实作:

import (
	"foo"

	"github.com/qiniu/text/tpl/interpreter"
)

func eval(text []byte, fname string) (..., err error) {

	defer func() {
		if e := recover(); e != nil {
			// 错误处理
			return
		}
	}()

	calc := foo.New()
	engine, err := interpreter.New(calc, nil)
	if err != nil {
		// 错误处理
		return
	}

	err = engine.MatchExtractly(text, fname)
	if err != nil {
		// 错误处理
		return
	}

	// ...
	return
}

Documentation

Index

Constants

View Source
const (
	ILLEGAL uint = iota
	EOF
	COMMENT

	IDENT  // main
	INT    // 12345
	FLOAT  // 123.45
	IMAG   // 123.45i
	CHAR   // 'a'
	STRING // "abc"

	ADD = '+'
	SUB = '-'
	MUL = '*'
	QUO = '/'
	REM = '%'

	AND = '&'
	OR  = '|'
	XOR = '^'

	LT     = '<'
	GT     = '>'
	ASSIGN = '='
	NOT    = '!'

	LPAREN = '('
	LBRACK = '['
	LBRACE = '{'
	COMMA  = ','
	PERIOD = '.'

	RPAREN    = ')'
	RBRACK    = ']'
	RBRACE    = '}'
	SEMICOLON = ';'
	COLON     = ':'
	QUESTION  = '?'
	TILDE     = '~'
	AT        = '@'
)
View Source
const (
	SHL     uint // <<
	SHR          // >>
	AND_NOT      // &^

	ADD_ASSIGN // +=
	SUB_ASSIGN // -=
	MUL_ASSIGN // *=
	QUO_ASSIGN // /=
	REM_ASSIGN // %=

	AND_ASSIGN     // &=
	OR_ASSIGN      // |=
	XOR_ASSIGN     // ^=
	SHL_ASSIGN     // <<=
	SHR_ASSIGN     // >>=
	AND_NOT_ASSIGN // &^=

	LAND  // &&
	LOR   // ||
	ARROW // <-
	INC   // ++
	DEC   // --

	EQ       // ==
	NE       // !=
	LE       // <=
	GE       // >=
	DEFINE   // :=
	ELLIPSIS // ...

	USER_TOKEN_BEGIN uint = 0xb0
)

Variables

View Source
var (
	// ErrVarNotAssigned error
	ErrVarNotAssigned = errors.New("variable is not assigned")
	// ErrVarAssigned error
	ErrVarAssigned = errors.New("variable is already assigned")
)
View Source
var (
	// ErrNoDoc error
	ErrNoDoc = errors.New("no doc")
)
View Source
var (
	// ErrNoGrammar error
	ErrNoGrammar = errors.New("no grammar")
)

Functions

func Code

func Code(g Grammar, t Tokener) string

Code returns the marshal result of a grammar unit.

func FileLine

func FileLine(doc []Token, t Tokener) (string, int)

FileLine returns the file name and line number of tokens.

func Lit2Token added in v1.9.2

func Lit2Token(lit string) (tok uint)

Lit2Token - literal => token

func Source

func Source(doc []Token, t Tokener) (text []byte)

Source returns the source code of tokens.

func Token2Lit added in v1.9.2

func Token2Lit(tok uint) (s string)

Token2Lit - token => literal

func TokenLen added in v1.9.1

func TokenLen(tok uint) int

TokenLen returns: 1) len of is token literal, if token is an operator. 2) 0 for else.

Types

type AutoKwScanner

type AutoKwScanner struct {
	Scanner
	// contains filtered or unexported fields
}

AutoKwScanner represents an auto-keyword discovering scanner.

func (*AutoKwScanner) Init

func (p *AutoKwScanner) Init(file *token.File, src []byte, err ScanErrorHandler, mode ScanMode)

Init initializes this scanner.

func (*AutoKwScanner) Ltot

func (p *AutoKwScanner) Ltot(lit string) (tok uint)

Ltot - convert literal to token.

func (*AutoKwScanner) Scan

func (p *AutoKwScanner) Scan() (t Token)

Scan scans next token.

func (*AutoKwScanner) Ttol

func (p *AutoKwScanner) Ttol(tok uint) (lit string)

Ttol - convert token to literal.

type CompileRet

type CompileRet struct {
	*Matcher
	Grammars map[string]Grammar
	Vars     map[string]*GrVar
}

CompileRet represents a compiling result.

func (CompileRet) EvalSub

func (p CompileRet) EvalSub(name string, src interface{}) error

EvalSub evals a sub routine specified by src.

type Compiler

type Compiler struct {
	Grammar  []byte
	Marker   func(g Grammar, mark string) Grammar
	Init     func()
	Scanner  Tokener
	ScanMode ScanMode
}

Compiler represents a compiler.

func (*Compiler) Cl

func (p *Compiler) Cl() (ret CompileRet, err error)

Cl compiles tpl code into grammar.

term = factor *(

'%' factor/list |
"%=" factor/list0 |
'/' IDENT/mark
)

expr = +(term | '!'/nil)/and

grammar = expr % '|'/or

doc = +((IDENT '=' grammar ';')/assign)

factor =

IDENT/ident |
CHAR/gr |
STRING/gr |
INT/true |
'*' factor/repeat0 |
'+' factor/repeat1 |
'?' factor/repeat01 |
'~' factor/not |
'@' factor/peek |
'(' grammar ')'

type Context

type Context interface {
	Tokener() Tokener
}

Context represents the matching context.

type GrNamed

type GrNamed struct {
	// contains filtered or unexported fields
}

GrNamed represents a named grammar.

func Named

func Named(name string, g Grammar) *GrNamed

Named creates a named grammar.

func (*GrNamed) Assign

func (p *GrNamed) Assign(name string, g Grammar)

Assign assigns this named grammar.

func (*GrNamed) Len

func (p *GrNamed) Len() int

Len returns -1.

func (*GrNamed) Marshal

func (p *GrNamed) Marshal(b []byte, t Tokener, lvlParent int) []byte

Marshal marshals grammar to be code.

func (*GrNamed) Match

func (p *GrNamed) Match(src []Token, ctx Context) (n int, err error)

Match matches tokens.

type GrVar

type GrVar struct {
	Elem Grammar
	Name string
}

GrVar represents a grammar variable.

func Var

func Var(name string) *GrVar

Var creates a grammar variable.

func (*GrVar) Assign

func (p *GrVar) Assign(g Grammar) error

Assign assigns this grammar variable.

func (*GrVar) Len

func (p *GrVar) Len() int

Len returns -1.

func (*GrVar) Marshal

func (p *GrVar) Marshal(b []byte, t Tokener, lvlParent int) []byte

Marshal marshals grammar to be code.

func (*GrVar) Match

func (p *GrVar) Match(src []Token, ctx Context) (n int, err error)

Match matches tokens.

type Grammar

type Grammar interface {
	Match(src []Token, ctx Context) (n int, err error)
	Marshal(b []byte, t Tokener, lvlParent int) []byte
	Len() int // 如果这个Grammar不是array型的,返回-1
}

Grammar represents a syntax unit.

var GrEOF Grammar = grEOF{}

GrEOF is a grammar that matching eof.

var GrTrue Grammar = grTrue{}

GrTrue is a grammar that always matching successfully.

func Action

func Action(g Grammar, act func(tokens []Token, g Grammar)) Grammar

Action represents G/action.

func And

func And(gs ...Grammar) Grammar

And represents G1 G2 ... Gn

func Clone

func Clone(gs []Grammar) []Grammar

Clone clones grammars.

func Gr

func Gr(tok uint) Grammar

Gr returns a grammar that matches a token.

func List

func List(a, b Grammar) Grammar

List represents G1 % G2

func List0

func List0(a, b Grammar) Grammar

List0 represents G1 %= G2

func Not

func Not(g Grammar) Grammar

Not returns a grammar that peeks next tokens unmatching a grammar or not.

func Or

func Or(gs ...Grammar) Grammar

Or represents G1 | G2 | ... | Gn

func Peek

func Peek(g Grammar) Grammar

Peek returns a grammar that peeks next tokens matching a grammar or not.

func Repeat0

func Repeat0(g Grammar) Grammar

Repeat0 represents *G

func Repeat01

func Repeat01(g Grammar) Grammar

Repeat01 represents ?G

func Repeat1

func Repeat1(g Grammar) Grammar

Repeat1 represents +G

func Transaction

func Transaction(
	g Grammar, begin func() interface{}, end func(trans interface{}, err error)) Grammar

Transaction represents G/Transaction.

type MatchError

type MatchError struct {
	Grammar Grammar
	Ctx     Context
	Src     []Token
	Err     error
	IdxErr  int
}

MatchError represents a matching error.

func (*MatchError) Error

func (p *MatchError) Error() string

type Matcher

type Matcher struct {
	Grammar  Grammar
	Scanner  Tokener
	ScanMode ScanMode
	Init     func()
}

Matcher represents a compiled grammar.

func (*Matcher) Code

func (p *Matcher) Code() string

Code returns tpl code of this grammar.

func (*Matcher) Eval

func (p *Matcher) Eval(src string) (err error)

Eval matches src exactly.

func (*Matcher) Match

func (p *Matcher) Match(src []byte, fname string) (next []Token, err error)

Match matches src.

func (*Matcher) MatchExactly

func (p *Matcher) MatchExactly(src []byte, fname string) (err error)

MatchExactly matches src exactly.

func (*Matcher) Tokener

func (p *Matcher) Tokener() Tokener

Tokener returns the scanner.

func (*Matcher) Tokenize

func (p *Matcher) Tokenize(src []byte, fname string) (tokens []Token, err error)

Tokenize tokenizes src.

type ScanErrorHandler

type ScanErrorHandler func(pos token.Position, msg string)

An ScanErrorHandler may be provided to Scanner.Init. If a syntax error is encountered and a handler was installed, the handler is called with a position and an error message. The position points to the beginning of the offending token.

type ScanMode

type ScanMode uint

A ScanMode value is a set of flags (or 0). They control scanner behavior.

const (
	// ScanComments means returning comments as COMMENT tokens
	ScanComments ScanMode = 1 << iota

	// InsertSemis means automatically insert semicolons
	InsertSemis
)

type Scanner

type Scanner struct {

	// public state - ok to modify
	ErrorCount int // number of errors encountered
	// contains filtered or unexported fields
}

A Scanner holds the scanner's internal state while processing a given text. It can be allocated as part of another data structure but must be initialized via Init before use.

func (*Scanner) Init

func (s *Scanner) Init(file *token.File, src []byte, err ScanErrorHandler, mode ScanMode)

Init prepares the scanner s to tokenize the text src by setting the scanner at the beginning of src. The scanner uses the file set file for position information and it adds line information for each line. It is ok to re-use the same file when re-scanning the same file as line information which is already present is ignored. Init causes a panic if the file size does not match the src size.

Calls to Scan will invoke the error handler err if they encounter a syntax error and err is not nil. Also, for each error encountered, the Scanner field ErrorCount is incremented by one. The mode parameter determines how comments are handled.

Note that Init may call err if there is an error in the first character of the file.

func (*Scanner) Ltot

func (s *Scanner) Ltot(lit string) (tok uint)

Ltot - literal => token

func (*Scanner) Scan

func (s *Scanner) Scan() (t Token)

Scan scans the next token and returns the token position, the token, and its literal string if applicable. The source end is indicated by token.EOF.

If the returned token is a literal (IDENT, INT, FLOAT, IMAG, CHAR, STRING) or COMMENT, the literal string has the corresponding value.

If the returned token is SEMICOLON, the corresponding literal string is ";" if the semicolon was present in the source, and "\n" if the semicolon was inserted because of a newline or at EOF.

If the returned token is ILLEGAL, the literal string is the offending character.

In all other cases, Scan returns an empty literal string.

For more tolerant parsing, Scan will return a valid token if possible even if a syntax error was encountered. Thus, even if the resulting token sequence contains no illegal tokens, a client may not assume that no error occurred. Instead it must check the scanner's ErrorCount or the number of calls of the error handler, if there was one installed.

Scan adds line information to the file added to the file set with Init. Token positions are relative to that file and thus relative to the file set.

func (*Scanner) Source

func (s *Scanner) Source() TokenSource

Source returns the scanning source.

func (*Scanner) Ttol

func (s *Scanner) Ttol(tok uint) string

Ttol - token => literal

type Token

type Token struct {
	Kind    uint
	Pos     token.Pos
	Literal string
}

A Token is a lexical unit returned by Scan.

func (*Token) End added in v1.9.1

func (p *Token) End() token.Pos

End returns end position of this token.

type TokenSource

type TokenSource struct {
	File *token.File
	Src  []byte
}

TokenSource represents the source file.

type Tokener

type Tokener interface {
	Scan() (t Token)
	Source() (src TokenSource)
	Ttol(tok uint) (lit string)
	Ltot(lit string) (tok uint)
	Init(file *token.File, src []byte, err ScanErrorHandler, mode ScanMode)
}

Tokener represents the scanner.

type TokenizeError

type TokenizeError struct {
	Pos token.Position
	Msg string
}

TokenizeError represents a tokenize error.

func (*TokenizeError) Error

func (p *TokenizeError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL