新增: u-tpl SQL 模板引擎完整实现
- Lexer/Parser/Executor 三阶段架构
- #{param} 参数化 + ${raw} 原样替换 + 白名单安全策略
- @if/@for/@tpl/@include/@namespace 控制流
- 表达式引擎: 比较、逻辑、nil 检查、len() 内置函数
- 支持 ?/$1/:1 多数据库占位符风格
- 零依赖,纯 Go 标准库实现
This commit is contained in:
564
internal/expr.go
Normal file
564
internal/expr.go
Normal file
@@ -0,0 +1,564 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type ExprParser struct {
|
||||
input []rune
|
||||
pos int
|
||||
line int
|
||||
col int
|
||||
}
|
||||
|
||||
func NewExprParser(input string, line, col int) *ExprParser {
|
||||
return &ExprParser{
|
||||
input: []rune(input),
|
||||
pos: 0,
|
||||
line: line,
|
||||
col: col,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ExprParser) Parse() (*Expr, error) {
|
||||
expr, err := p.parseOr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return expr, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseOr() (*Expr, error) {
|
||||
left, err := p.parseAnd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
p.skipSpaces()
|
||||
if !p.peekStr("||") {
|
||||
break
|
||||
}
|
||||
p.skip(2)
|
||||
p.skipSpaces()
|
||||
right, err := p.parseAnd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
left = &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprBinary, Left: left, Op: "||", Right: right}
|
||||
}
|
||||
return left, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseAnd() (*Expr, error) {
|
||||
left, err := p.parseCompare()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
p.skipSpaces()
|
||||
if !p.peekStr("&&") {
|
||||
break
|
||||
}
|
||||
p.skip(2)
|
||||
p.skipSpaces()
|
||||
right, err := p.parseCompare()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
left = &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprBinary, Left: left, Op: "&&", Right: right}
|
||||
}
|
||||
return left, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseCompare() (*Expr, error) {
|
||||
left, err := p.parseUnary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.skipSpaces()
|
||||
op := ""
|
||||
if p.peekStr("==") {
|
||||
op = "=="
|
||||
p.skip(2)
|
||||
} else if p.peekStr("!=") {
|
||||
op = "!="
|
||||
p.skip(2)
|
||||
} else if p.peekStr("<=") {
|
||||
op = "<="
|
||||
p.skip(2)
|
||||
} else if p.peekStr(">=") {
|
||||
op = ">="
|
||||
p.skip(2)
|
||||
} else if p.peekStr("<") {
|
||||
op = "<"
|
||||
p.skip(1)
|
||||
} else if p.peekStr(">") {
|
||||
op = ">"
|
||||
p.skip(1)
|
||||
}
|
||||
if op == "" {
|
||||
return left, nil
|
||||
}
|
||||
p.skipSpaces()
|
||||
right, err := p.parseUnary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprBinary, Left: left, Op: op, Right: right}, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseUnary() (*Expr, error) {
|
||||
p.skipSpaces()
|
||||
if p.peekStr("!") && !p.peekStr("!=") {
|
||||
p.skip(1)
|
||||
operand, err := p.parseUnary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprUnary, UnaryOp: "!", Operand: operand}, nil
|
||||
}
|
||||
return p.parsePrimary()
|
||||
}
|
||||
|
||||
func (p *ExprParser) parsePrimary() (*Expr, error) {
|
||||
p.skipSpaces()
|
||||
if p.pos >= len(p.input) {
|
||||
return nil, fmt.Errorf("line %d, col %d: unexpected end of expression", p.line, p.col)
|
||||
}
|
||||
ch := p.input[p.pos]
|
||||
|
||||
if ch == '"' {
|
||||
return p.parseStringLit()
|
||||
}
|
||||
if ch == '\'' {
|
||||
return p.parseSingleQuoteStringLit()
|
||||
}
|
||||
if ch >= '0' && ch <= '9' {
|
||||
return p.parseNumberLit()
|
||||
}
|
||||
if isIdentStart(ch) {
|
||||
return p.parseIdentOrKeyword()
|
||||
}
|
||||
if ch == '(' {
|
||||
p.skip(1)
|
||||
p.skipSpaces()
|
||||
expr, err := p.parseOr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.skipSpaces()
|
||||
if p.pos >= len(p.input) || p.input[p.pos] != ')' {
|
||||
return nil, fmt.Errorf("line %d, col %d: expected ')'", p.line, p.col)
|
||||
}
|
||||
p.skip(1)
|
||||
return expr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("line %d, col %d: unexpected character %q", p.line, p.col, string(ch))
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseIdentOrKeyword() (*Expr, error) {
|
||||
start := p.pos
|
||||
for p.pos < len(p.input) && isIdentPart(p.input[p.pos]) {
|
||||
p.pos++
|
||||
}
|
||||
name := string(p.input[start:p.pos])
|
||||
|
||||
switch name {
|
||||
case "true":
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: true}, nil
|
||||
case "false":
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: false}, nil
|
||||
case "nil":
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprNil}, nil
|
||||
}
|
||||
|
||||
p.skipSpaces()
|
||||
if p.pos < len(p.input) && p.input[p.pos] == '(' {
|
||||
return p.parseFuncCall(name)
|
||||
}
|
||||
|
||||
p.skipSpaces()
|
||||
varName := name
|
||||
for p.pos < len(p.input) && p.input[p.pos] == '.' {
|
||||
p.skip(1)
|
||||
if p.pos >= len(p.input) || !isIdentStart(p.input[p.pos]) {
|
||||
return nil, fmt.Errorf("line %d, col %d: expected identifier after '.'", p.line, p.col)
|
||||
}
|
||||
segStart := p.pos
|
||||
for p.pos < len(p.input) && isIdentPart(p.input[p.pos]) {
|
||||
p.pos++
|
||||
}
|
||||
varName += "." + string(p.input[segStart:p.pos])
|
||||
p.skipSpaces()
|
||||
}
|
||||
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprVariable, Name: varName}, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseFuncCall(name string) (*Expr, error) {
|
||||
p.skip(1)
|
||||
var args []*Expr
|
||||
p.skipSpaces()
|
||||
if p.pos < len(p.input) && p.input[p.pos] != ')' {
|
||||
for {
|
||||
arg, err := p.parseOr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, arg)
|
||||
p.skipSpaces()
|
||||
if p.pos < len(p.input) && p.input[p.pos] == ',' {
|
||||
p.skip(1)
|
||||
p.skipSpaces()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
p.skipSpaces()
|
||||
if p.pos >= len(p.input) || p.input[p.pos] != ')' {
|
||||
return nil, fmt.Errorf("line %d, col %d: expected ')' after function call", p.line, p.col)
|
||||
}
|
||||
p.skip(1)
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprFuncCall, FuncName: name, FuncArgs: args}, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseStringLit() (*Expr, error) {
|
||||
p.skip(1)
|
||||
var buf strings.Builder
|
||||
for p.pos < len(p.input) && p.input[p.pos] != '"' {
|
||||
if p.input[p.pos] == '\\' && p.pos+1 < len(p.input) {
|
||||
p.pos++
|
||||
switch p.input[p.pos] {
|
||||
case 'n':
|
||||
buf.WriteRune('\n')
|
||||
case 't':
|
||||
buf.WriteRune('\t')
|
||||
case '\\':
|
||||
buf.WriteRune('\\')
|
||||
case '"':
|
||||
buf.WriteRune('"')
|
||||
default:
|
||||
buf.WriteRune('\\')
|
||||
buf.WriteRune(p.input[p.pos])
|
||||
}
|
||||
} else {
|
||||
buf.WriteRune(p.input[p.pos])
|
||||
}
|
||||
p.pos++
|
||||
}
|
||||
if p.pos >= len(p.input) {
|
||||
return nil, fmt.Errorf("line %d, col %d: unterminated string", p.line, p.col)
|
||||
}
|
||||
p.skip(1)
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: buf.String()}, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseSingleQuoteStringLit() (*Expr, error) {
|
||||
p.skip(1)
|
||||
var buf strings.Builder
|
||||
for p.pos < len(p.input) && p.input[p.pos] != '\'' {
|
||||
buf.WriteRune(p.input[p.pos])
|
||||
p.pos++
|
||||
}
|
||||
if p.pos >= len(p.input) {
|
||||
return nil, fmt.Errorf("line %d, col %d: unterminated string", p.line, p.col)
|
||||
}
|
||||
p.skip(1)
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: buf.String()}, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) parseNumberLit() (*Expr, error) {
|
||||
start := p.pos
|
||||
for p.pos < len(p.input) && p.input[p.pos] >= '0' && p.input[p.pos] <= '9' {
|
||||
p.pos++
|
||||
}
|
||||
isFloat := false
|
||||
if p.pos < len(p.input) && p.input[p.pos] == '.' {
|
||||
next := p.pos + 1
|
||||
if next < len(p.input) && p.input[next] >= '0' && p.input[next] <= '9' {
|
||||
isFloat = true
|
||||
p.pos++
|
||||
for p.pos < len(p.input) && p.input[p.pos] >= '0' && p.input[p.pos] <= '9' {
|
||||
p.pos++
|
||||
}
|
||||
}
|
||||
}
|
||||
text := string(p.input[start:p.pos])
|
||||
if isFloat {
|
||||
v, err := strconv.ParseFloat(text, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("line %d, col %d: invalid number %q", p.line, p.col, text)
|
||||
}
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: v}, nil
|
||||
}
|
||||
v, err := strconv.ParseInt(text, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("line %d, col %d: invalid number %q", p.line, p.col, text)
|
||||
}
|
||||
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: int(v)}, nil
|
||||
}
|
||||
|
||||
func (p *ExprParser) peekStr(s string) bool {
|
||||
if p.pos+len(s) > len(p.input) {
|
||||
return false
|
||||
}
|
||||
for i, ch := range s {
|
||||
if p.input[p.pos+i] != ch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ExprParser) skip(n int) {
|
||||
p.pos += n
|
||||
p.col += n
|
||||
}
|
||||
|
||||
func (p *ExprParser) skipSpaces() {
|
||||
for p.pos < len(p.input) && unicode.IsSpace(p.input[p.pos]) {
|
||||
if p.input[p.pos] == '\n' {
|
||||
p.line++
|
||||
p.col = 0
|
||||
} else {
|
||||
p.col++
|
||||
}
|
||||
p.pos++
|
||||
}
|
||||
}
|
||||
|
||||
func isIdentStart(ch rune) bool {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
|
||||
}
|
||||
|
||||
func isIdentPart(ch rune) bool {
|
||||
return isIdentStart(ch) || (ch >= '0' && ch <= '9')
|
||||
}
|
||||
|
||||
func Eval(expr *Expr, ctx *Context) (any, error) {
|
||||
switch expr.ExprType {
|
||||
case ExprLiteral:
|
||||
return expr.Value, nil
|
||||
case ExprNil:
|
||||
return nil, nil
|
||||
case ExprVariable:
|
||||
val, ok := ctx.Get(expr.Name)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return val, nil
|
||||
case ExprUnary:
|
||||
return evalUnary(expr, ctx)
|
||||
case ExprBinary:
|
||||
return evalBinary(expr, ctx)
|
||||
case ExprFuncCall:
|
||||
return evalFuncCall(expr, ctx)
|
||||
default:
|
||||
return nil, fmt.Errorf("line %d, col %d: unknown expression type", expr.Pos.Line, expr.Pos.Col)
|
||||
}
|
||||
}
|
||||
|
||||
func evalUnary(expr *Expr, ctx *Context) (any, error) {
|
||||
val, err := Eval(expr.Operand, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch expr.UnaryOp {
|
||||
case "!":
|
||||
return !isTruthy(val), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("line %d, col %d: unknown unary operator %q", expr.Pos.Line, expr.Pos.Col, expr.UnaryOp)
|
||||
}
|
||||
}
|
||||
|
||||
func evalBinary(expr *Expr, ctx *Context) (any, error) {
|
||||
switch expr.Op {
|
||||
case "&&":
|
||||
left, err := Eval(expr.Left, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isTruthy(left) {
|
||||
return left, nil
|
||||
}
|
||||
return Eval(expr.Right, ctx)
|
||||
case "||":
|
||||
left, err := Eval(expr.Left, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isTruthy(left) {
|
||||
return left, nil
|
||||
}
|
||||
return Eval(expr.Right, ctx)
|
||||
}
|
||||
|
||||
left, err := Eval(expr.Left, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := Eval(expr.Right, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch expr.Op {
|
||||
case "==":
|
||||
return compareEqual(left, right), nil
|
||||
case "!=":
|
||||
return !compareEqual(left, right), nil
|
||||
case "<":
|
||||
return compareOrder(left, right, expr.Op)
|
||||
case ">":
|
||||
return compareOrder(left, right, expr.Op)
|
||||
case "<=":
|
||||
return compareOrder(left, right, expr.Op)
|
||||
case ">=":
|
||||
return compareOrder(left, right, expr.Op)
|
||||
default:
|
||||
return nil, fmt.Errorf("line %d, col %d: unknown operator %q", expr.Pos.Line, expr.Pos.Col, expr.Op)
|
||||
}
|
||||
}
|
||||
|
||||
func evalFuncCall(expr *Expr, ctx *Context) (any, error) {
|
||||
fn, ok := LookupBuiltin(expr.FuncName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("line %d, col %d: unknown function %q", expr.Pos.Line, expr.Pos.Col, expr.FuncName)
|
||||
}
|
||||
var args []any
|
||||
for _, a := range expr.FuncArgs {
|
||||
val, err := Eval(a, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, val)
|
||||
}
|
||||
result, ok := fn(args)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("line %d, col %d: function %q call failed", expr.Pos.Line, expr.Pos.Col, expr.FuncName)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isTruthy(val any) bool {
|
||||
if val == nil {
|
||||
return false
|
||||
}
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case int:
|
||||
return v != 0
|
||||
case int64:
|
||||
return v != 0
|
||||
case float64:
|
||||
return v != 0
|
||||
case string:
|
||||
return v != ""
|
||||
case []any:
|
||||
return len(v) > 0
|
||||
case map[string]any:
|
||||
return len(v) > 0
|
||||
default:
|
||||
rv := reflect.ValueOf(val)
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice, reflect.Array, reflect.Map:
|
||||
return rv.Len() > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compareEqual(left, right any) bool {
|
||||
if left == nil && right == nil {
|
||||
return true
|
||||
}
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lbool, lbOk := left.(bool)
|
||||
rbool, rbOk := right.(bool)
|
||||
if lbOk || rbOk {
|
||||
return lbOk && rbOk && lbool == rbool
|
||||
}
|
||||
|
||||
lf, lok := toFloat64(left)
|
||||
rf, rok := toFloat64(right)
|
||||
if lok && rok {
|
||||
return lf == rf
|
||||
}
|
||||
|
||||
lstr, lsOk := left.(string)
|
||||
rstr, rsOk := right.(string)
|
||||
if lsOk && rsOk {
|
||||
return lstr == rstr
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right)
|
||||
}
|
||||
|
||||
func compareOrder(left, right any, op string) (bool, error) {
|
||||
if left == nil || right == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
lf, lok := toFloat64(left)
|
||||
rf, rok := toFloat64(right)
|
||||
if lok && rok {
|
||||
switch op {
|
||||
case "<":
|
||||
return lf < rf, nil
|
||||
case ">":
|
||||
return lf > rf, nil
|
||||
case "<=":
|
||||
return lf <= rf, nil
|
||||
case ">=":
|
||||
return lf >= rf, nil
|
||||
}
|
||||
}
|
||||
|
||||
ls, lsOk := left.(string)
|
||||
rs, rsOk := right.(string)
|
||||
if lsOk && rsOk {
|
||||
switch op {
|
||||
case "<":
|
||||
return ls < rs, nil
|
||||
case ">":
|
||||
return ls > rs, nil
|
||||
case "<=":
|
||||
return ls <= rs, nil
|
||||
case ">=":
|
||||
return ls >= rs, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("line 0, col 0: cannot compare %T and %T with %s", left, right, op)
|
||||
}
|
||||
|
||||
func toFloat64(val any) (float64, bool) {
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return float64(v), true
|
||||
case int64:
|
||||
return float64(v), true
|
||||
case float64:
|
||||
return v, true
|
||||
case float32:
|
||||
return float64(v), true
|
||||
case uint:
|
||||
return float64(v), true
|
||||
case uint64:
|
||||
return float64(v), true
|
||||
case int32:
|
||||
return float64(v), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user