本示例主要作为备忘使用。
目前考虑的计算器主要支持四则运算和括号,从实现的角度来说。本示例主要由三个部分组成,词法分
析模块、语法分析模块、回归测试模块。
词法分析模块:词法分析模块主要将输入的表达式转化为一个个的 token 转交语法分析模块处理。
比如表达式 “1”这个被词法分析解析成一个 “NUMBER” 类型,并将该类型的值 1 存储在 lval.num 中,
这个变量定义在 语法分析模块中的 %union 域中。
// filename: calcLex.go
package simplelex
import (
"text/scanner"
"log"
"strconv"
"strings"
)
var LexPrintToken = false
type Token struct {
Type int
Str string
}
type simpleLex struct {
scanner.Scanner
value float64
}
// 这个接口必须实现,是词法分析的入口
func (s *simpleLex) Lex(lval *yySymType) int {
r, lit := s.Scan(), s.TokenText()
var token Token
token.Str = lit
switch r {
case scanner.EOF:
return 0
case scanner.Int:
i, _ := strconv.Atoi(lit)
lval.num = float64(i)
token.Type = scanner.Float
case scanner.Float:
lval.num, _ = strconv.ParseFloat(lit, 64)
token.Type = scanner.Float
default:
token.Type = int(r)
}
// if LexPrintToken {
// fmt.Printf("<<token: %s, %s>>\r\n",scanner.TokenString(token.type), token.Str)
// }
if token.Type == scanner.Float {
return NUMBER
} else {
return token.Type
}
// return token.Type
}
// 词法分析异常处理 该接口必须实现
func (s *simpleLex) Error(s1 string) {
log.Printf("parse error: %s", s1)
}
// 计算入口
func Parse(code string) float64{
s := new(simpleLex)
s.Init(strings.NewReader(code))
yyParse(s)
return s.value
}
语法分析模块(filename: calcyacc.y):这个模块本质就是按照 yacc 的语法定义文法,goyacc 会将该文件转化为一
个分析引 擎。该文件中定义了 %type <num> 或 %token <num> 表示文法中出现了定义的这些非终结符或终结符将
默认访问 num字段。 若不这样写,在文法动作中需要使用 $1.num 使用这个变量。表达式的最终值通过 yylex 这个
接口强转类型为 simpleLex(该类型定义在词法分析文件中),并将值保存在 simpleLex 的 num 域中。
%{
package simplelex
%}
%union {
num float64
}
%type <num> expression term factor
%token '+' '-' '*' '/' '(' ')'
%token <num> NUMBER
%%
top : expression
{
if l, ok := yylex.(*simpleLex); ok {
l.value = $1
}
}
;
expression : expression '+' term
{ $$ = $1 + $3 }
| expression '-' term
{ $$ = $1 - $3 }
| term
{ $$ = $1 }
;
term : term '*' factor
{ $$ = $1 * $3 }
| term '/' factor
{ $$ = $1 / $3 }
| factor
{ $$ = $1 }
;
factor : NUMBER
{ $$ = $1 }
| '(' expression ')'
{ $$ = $2 }
;
%%
回归测试模块:这里只是利用 golang 语言自带的 testing 模块测试用例是否符合预期。在开展类似的分析项目的时候,回归测试的构建是很有必要的。原因是文法具备牵一发而动全身的特性,必须要谨慎的保证对文法的变更不影响已满足的特性。而对于大型的分析项目的构建,往往不能一次性达成目标。而是一个小目标的形式逐步推进。而回归测试时保证特定的唯一保证。
// filename: calcLex_test.go
package simplelex
import (
"testing"
)
func TestParse(t *testing.T) {
tests := []struct {
code string
value float64
}{
{"1", 1},
{"1+1", 2},
{"1+2*3", 7},
{"1+3/1", 4},
{"1-1", 0},
{"(1+2)*3", 9},
{"(1+2)/3", 1},
{"1+(2*3)", 7},
{"3*2+1", 7},
{"3*(2+1)", 9},
{"1*2*3", 6},
{"1+2+3", 6},
{"1/2", 0.5},
{"2/3", 0.6666666666666666},
}
for _, test := range tests {
if value := Parse(test.code); value != test.value {
t.Errorf("err Actual: %.16f Expect: %f", value, test.value)
} else {
t.Logf(" sucess %s = %f", test.code, test.value)
}
}
}
如何使用它: goyacc -o calcyacc.go calcyacc.y && go test -v
正常情况下你将会看到下面这个页面: