Repository: goplus/gop
Pull request #2200 (2025-03-25 21:44:16)

tpl/variant: module/fncall; tpl/matcher: Var.Match recover error; tpl: this => self
* tpl/variant: module/fncall
* tpl/matcher: Var.Match recover error
* tpl: this => self


Patch:
diff --git a/cl/expr.go b/cl/expr.go
index b99bc821..1bd0b6b3 100644
--- a/cl/expr.go
+++ b/cl/expr.go
@@ -1136,7 +1136,7 @@ func compileDomainTextLit(ctx *blockCtx, v *ast.DomainTextLit) {
 func lambdaRetFunc(expr *ast.LambdaExpr2) *ast.LambdaExpr2 {
 	v := *expr
 	v.Lhs = []*ast.Ident{
-		{NamePos: expr.Pos(), Name: "this"},
+		{NamePos: expr.Pos(), Name: "self"},
 	}
 	return &v
 }
diff --git a/demo/tpl-calc/calc.gop b/demo/tpl-calc/calc.gop
index a0adc502..2326ab3c 100644
--- a/demo/tpl-calc/calc.gop
+++ b/demo/tpl-calc/calc.gop
@@ -6,7 +6,7 @@ import (
 cl := tpl`
 
 expr = operand % ("*" | "/") % ("+" | "-") => {
-	return tpl.binaryOp(true, this, (op, x, y) => {
+	return tpl.binaryOp(true, self, (op, x, y) => {
 		switch op.Tok {
 		case '+': return x.(float64) + y.(float64)
 		case '-': return x.(float64) - y.(float64)
@@ -20,11 +20,11 @@ expr = operand % ("*" | "/") % ("+" | "-") => {
 operand = basicLit | unaryExpr
 
 unaryExpr = "-" operand => {
-	return -(this[1].(float64))
+	return -(self[1].(float64))
 }
 
 basicLit = INT | FLOAT => {
-	return this.(*tpl.Token).Lit.float!
+	return self.(*tpl.Token).Lit.float!
 }
 `!
 
diff --git a/demo/tpl-gen-ast/gen_calc_ast.gop b/demo/tpl-gen-ast/gen_calc_ast.gop
index d38dec3a..d323144a 100644
--- a/demo/tpl-gen-ast/gen_calc_ast.gop
+++ b/demo/tpl-gen-ast/gen_calc_ast.gop
@@ -29,7 +29,7 @@ type BasicLit struct {
 cl := tpl`
 
 expr = operand % ("*" | "/") % ("+" | "-") => {
-	return tpl.binaryOp(true, this, (op, x, y) => {
+	return tpl.binaryOp(true, self, (op, x, y) => {
 		return &BinaryExpr{
 			X:     x,
 			OpPos: op.Pos,
@@ -42,16 +42,16 @@ expr = operand % ("*" | "/") % ("+" | "-") => {
 operand = basicLit | unaryExpr
 
 unaryExpr = "-" operand => {
-	op := this[0].(*tpl.Token)
+	op := self[0].(*tpl.Token)
 	return &UnaryExpr{
 		OpPos: op.Pos,
 		Op:    op.Tok,
-		X:     this[1],
+		X:     self[1],
 	}
 }
 
 basicLit = INT | FLOAT => {
-	op := this.(*tpl.Token)
+	op := self.(*tpl.Token)
 	return &BasicLit{
 		ValuePos: op.Pos,
 		Kind:     op.Tok,
diff --git a/demo/tpl-vcalc/variant_calc.gop b/demo/tpl-vcalc/variant_calc.gop
index 74b53fa8..ca95e9dd 100644
--- a/demo/tpl-vcalc/variant_calc.gop
+++ b/demo/tpl-vcalc/variant_calc.gop
@@ -3,12 +3,15 @@ import (
 	"gop/tpl/token"
 	"gop/tpl/variant"
 	"os"
+
+	_ "gop/tpl/variant/math"
+	_ "gop/tpl/variant/time"
 )
 
 cl := tpl`
 
 expr = cmpExpr % ("||" | "&&" | and | or) => {
-	return tpl.binaryOp(false, this, (op, x, y) => {
+	return tpl.binaryOp(false, self, (op, x, y) => {
 		return variant.logicOp(op.Tok, x, y)
 	})
 }
@@ -22,22 +25,36 @@ or = "or" | "OR" => {
 }
 
 cmpExpr = mathExpr % ("==" | "=" | "!=" | "<>" | "<" | "<=" | ">" | ">=") => {
-	return tpl.binaryOp(false, this, (op, x, y) => {
+	return tpl.binaryOp(false, self, (op, x, y) => {
 		return variant.compare(op.Tok, x, y)
 	})
 }
 
 mathExpr = operand % ("*" | "/" | "%") % ("+" | "-") => {
-	return tpl.binaryOp(true, this, (op, x, y) => {
+	return tpl.binaryOp(true, self, (op, x, y) => {
 		return variant.mathOp(op.Tok, x, y)
 	})
 }
 
-operand = basicLit | unaryExpr
+operand = basicLit | parenExpr | unaryExpr | callExpr
+
+parenExpr = "(" expr ")" => {
+	return self[1]
+}
 
 unaryExpr = ("-" | "+" | "!") operand => {
-	op := this[0].(*tpl.Token)
-	return variant.unaryOp(op.Tok, this[1])
+	op := self[0].(*tpl.Token)
+	return variant.unaryOp(op.Tok, self[1])
+}
+
+callExpr = IDENT "(" ?arglist ")" => {
+	fn := self[0].(*tpl.Token).Lit
+	args, _ := self[2].([]any)
+	return variant.call(fn, args...)
+}
+
+arglist = expr % "," => {
+	return tpl.list(self)
 }
 
 basicLit = intVal | floatVal | stringVal | true | false
@@ -51,18 +68,20 @@ false = "false" => {
 }
 
 stringVal = STRING => {
-	return this.(*tpl.Token).Lit.unquote!
+	return self.(*tpl.Token).Lit.unquote!
 }
 
 floatVal = FLOAT => {
-	return this.(*tpl.Token).Lit.float!
+	return self.(*tpl.Token).Lit.float!
 }
 
 intVal = INT => {
-	return this.(*tpl.Token).Lit.int!
+	return self.(*tpl.Token).Lit.int!
 }
 `!
 
+variant.initUniverse "math", "time"
+
 print "> "
 for line in os.Stdin {
 	e, err := cl.parseExpr(line, nil)
diff --git a/tpl/matcher/match.go b/tpl/matcher/match.go
index 5a0cbbc2..23ad7b2d 100644
--- a/tpl/matcher/match.go
+++ b/tpl/matcher/match.go
@@ -38,6 +38,9 @@ type Error struct {
 	Fset *token.FileSet
 	Pos  token.Pos
 	Msg  string
+
+	// is a runtime error
+	Dyn bool
 }
 
 func (p *Error) Error() string {
@@ -45,6 +48,13 @@ func (p *Error) Error() string {
 	return fmt.Sprintf("%v: %s", pos, p.Msg)
 }
 
+func isDyn(err error) bool {
+	if e, ok := err.(*Error); ok {
+		return e.Dyn
+	}
+	return false
+}
+
 // -----------------------------------------------------------------------------
 
 // Context represents the context of a matching process.
@@ -74,12 +84,12 @@ func (p *Context) SetLastError(left int, err error) {
 
 // NewError creates a new error.
 func (p *Context) NewError(pos token.Pos, msg string) *Error {
-	return &Error{p.Fset, pos, msg}
+	return &Error{p.Fset, pos, msg, false}
 }
 
 // NewErrorf creates a new error with a format string.
 func (p *Context) NewErrorf(pos token.Pos, format string, args ...any) error {
-	return &Error{p.Fset, pos, fmt.Sprintf(format, args...)}
+	return &Error{p.Fset, pos, fmt.Sprintf(format, args...), false}
 }
 
 // -----------------------------------------------------------------------------
@@ -194,7 +204,11 @@ func (p *gSequence) Match(src []*types.Token, ctx *Context) (n int, result any,
 	for i, g := range p.items {
 		n1, ret1, err1 := g.Match(src[n:], ctx)
 		if err1 != nil {
-			return n + n1, nil, err1
+			if isDyn(err1) {
+				err = err1
+			} else {
+				return n + n1, nil, err1
+			}
 		}
 		rets[i] = ret1
 		n += n1
@@ -224,9 +238,13 @@ func (p *gRepeat0) Match(src []*types.Token, ctx *Context) (n int, result any, e
 	for {
 		n1, ret1, err1 := g.Match(src, ctx)
 		if err1 != nil {
-			ctx.SetLastError(len(src)-n1, err1)
-			result = rets
-			return
+			if isDyn(err1) {
+				err = err1
+			} else {
+				ctx.SetLastError(len(src)-n1, err1)
+				result = rets
+				return
+			}
 		}
 		rets = append(rets, ret1)
 		n += n1
@@ -261,9 +279,13 @@ func (p *gRepeat1) Match(src []*types.Token, ctx *Context) (n int, result any, e
 	for {
 		n1, ret1, err1 := g.Match(src[n:], ctx)
 		if err1 != nil {
-			ctx.SetLastError(len(src)-n-n1, err1)
-			result = rets
-			return
+			if isDyn(err1) {
+				err = err1
+			} else {
+				ctx.SetLastError(len(src)-n-n1, err1)
+				result = rets
+				return
+			}
 		}
 		rets = append(rets, ret1)
 		n += n1
@@ -330,6 +352,26 @@ func (p *Var) Match(src []*types.Token, ctx *Context) (n int, result any, err er
 	n, result, err = g.Match(src, ctx)
 	if err == nil {
 		if retProc := p.RetProc; retProc != nil {
+			defer func() {
+				if e := recover(); e != nil {
+					switch e := e.(type) {
+					case *Error:
+						if e.Fset == nil {
+							e.Fset = ctx.Fset
+						}
+						err = e
+					case string:
+						err = &Error{
+							Fset: ctx.Fset,
+							Pos:  src[0].Pos,
+							Msg:  e,
+							Dyn:  true,
+						}
+					default:
+						err = e.(error)
+					}
+				}
+			}()
 			if g.IsList() {
 				result = retProc.(ListRetProc)(result.([]any))
 			} else {
diff --git a/tpl/tpl.go b/tpl/tpl.go
index 756cc61b..9fa2d15e 100644
--- a/tpl/tpl.go
+++ b/tpl/tpl.go
@@ -36,7 +36,6 @@ import (
 // Compiler represents a TPL compiler.
 type Compiler struct {
 	cl.Result
-	Fset *token.FileSet
 }
 
 // New creates a new TPL compiler.
@@ -57,7 +56,6 @@ func FromFile(fset *token.FileSet, filename string, src any, params ...any) (ret
 		return
 	}
 	ret.Result, err = cl.NewEx(retProcs(params), fset, f)
-	ret.Fset = fset
 	return
 }
 
@@ -90,8 +88,9 @@ type Scanner interface {
 // Config represents a parsing configuration of [Compiler.Parse].
 type Config struct {
 	Scanner          Scanner
-	ScanMode         scanner.Mode
 	ScanErrorHandler scanner.ErrorHandler
+	ScanMode         scanner.Mode
+	Fset             *token.FileSet
 }
 
 // ParseExpr parses an expression.
@@ -109,7 +108,7 @@ func (p *Compiler) ParseExprFrom(filename string, src any, conf *Config) (result
 		return
 	}
 	t := ms.Next()
-	err = &matcher.Error{Fset: p.Fset, Pos: t.Pos, Msg: fmt.Sprintf("unexpected token: %v", t)}
+	err = ms.Ctx.NewErrorf(t.Pos, "unexpected token: %v", t)
 	return
 }
 
@@ -121,7 +120,7 @@ func (p *Compiler) Parse(filename string, src any, conf *Config) (result any, er
 	}
 	if len(ms.Toks) > ms.N {
 		t := ms.Next()
-		err = &matcher.Error{Fset: p.Fset, Pos: t.Pos, Msg: fmt.Sprintf("unexpected token: %v", t)}
+		err = ms.Ctx.NewErrorf(t.Pos, "unexpected token: %v", t)
 	}
 	return
 }
@@ -155,7 +154,10 @@ func (p *Compiler) Match(filename string, src any, conf *Config) (ms MatchState,
 	if s == nil {
 		s = new(scanner.Scanner)
 	}
-	fset := p.Fset
+	fset := conf.Fset
+	if fset == nil {
+		fset = token.NewFileSet()
+	}
 	f := fset.AddFile(filename, fset.Base(), len(b))
 	s.Init(f, b, conf.ScanErrorHandler, conf.ScanMode)
 	n := (len(b) >> 3) &^ 7
@@ -357,7 +359,7 @@ func BinaryOpR(in []any, fn func(op *Token, x, y any) any) any {
 		if v, ok := y.([]any); ok {
 			y = BinaryOpR(v, fn)
 		}
-		ret = fn(op, ret, y)
+		ret = fncall(fn, op, ret, y)
 	}
 	return ret
 }
@@ -368,11 +370,34 @@ func BinaryOpNR(in []any, fn func(op *Token, x, y any) any) any {
 		next := v.([]any)
 		op := next[0].(*Token)
 		y := next[1]
-		ret = fn(op, ret, y)
+		ret = fncall(fn, op, ret, y)
 	}
 	return ret
 }
 
+func fncall(fn func(op *Token, x, y any) any, op *Token, x, y any) any {
+	defer func() {
+		if e := recover(); e != nil {
+			panic(toErr(e, op))
+		}
+	}()
+	return fn(op, x, y)
+}
+
+func toErr(e any, op *Token) error {
+	switch e := e.(type) {
+	case *matcher.Error:
+		return e
+	case string:
+		return &matcher.Error{
+			Pos: op.Pos,
+			Msg: e,
+			Dyn: true,
+		}
+	}
+	return e.(error)
+}
+
 // UnaryExpr converts the matching result of (op X) to a unary expression.
 func UnaryExpr(in []any) ast.Expr {
 	op := in[0].(*Token)
@@ -403,3 +428,14 @@ func BasicLit(this any) *ast.BasicLit {
 }
 
 // -----------------------------------------------------------------------------
+
+func Panic(pos token.Pos, msg string) {
+	err := &matcher.Error{
+		Pos: pos,
+		Msg: msg,
+		Dyn: true,
+	}
+	panic(err)
+}
+
+// -----------------------------------------------------------------------------
diff --git a/tpl/variant/math/math.go b/tpl/variant/math/math.go
new file mode 100644
index 00000000..bfebb80a
--- /dev/null
+++ b/tpl/variant/math/math.go
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package math
+
+import (
+	"math"
+
+	"github.com/goplus/gop/tpl/token"
+	"github.com/goplus/gop/tpl/variant"
+)
+
+// -----------------------------------------------------------------------------
+
+type namedFn struct {
+	name string
+	fn   func(...any) any
+}
+
+type math1 struct {
+	name string
+	fn   func(float64) float64
+}
+
+type math2 struct {
+	name string
+	fn   func(x, y float64) float64
+}
+
+type mathIntFloat struct {
+	name string
+	fn   func(x int, y float64) float64
+}
+
+func float1(args []any) float64 {
+	if len(args) != 1 {
+		panic("function call: arity mismatch")
+	}
+	return variant.Float(args[0])
+}
+
+func float2(args []any) (float64, float64) {
+	if len(args) != 2 {
+		panic("function call: arity mismatch")
+	}
+	return variant.Float(args[0]), variant.Float(args[1])
+}
+
+func intFloat(args []any) (int, float64) {
+	if len(args) != 2 {
+		panic("function call: arity mismatch")
+	}
+	return variant.Int(args[0]), variant.Float(args[1])
+}
+
+func regMath1(mod *variant.Module, name string, fn func(float64) float64) {
+	mod.Insert(name, func(args ...any) any {
+		return fn(float1(args))
+	})
+}
+
+func regMath2(mod *variant.Module, name string, fn func(x, y float64) float64) {
+	mod.Insert(name, func(args ...any) any {
+		return fn(float2(args))
+	})
+}
+
+func regMathIntFloat(mod *variant.Module, name string, fn func(x int, y float64) float64) {
+	mod.Insert(name, func(args ...any) any {
+		return fn(intFloat(args))
+	})
+}
+
+// -----------------------------------------------------------------------------
+
+func topValue(op token.Token, args []any) any {
+	ret := variant.Eval(args[0])
+	for _, v := range args[1:] {
+		if variant.Compare(op, v, ret) {
+			ret = v
+		}
+	}
+	return ret
+}
+
+func Max(args ...any) any {
+	if len(args) == 0 {
+		panic("max: arity mismatch")
+	}
+	return topValue(token.GT, args)
+}
+
+func Min(args ...any) any {
+	if len(args) == 0 {
+		panic("min: arity mismatch")
+	}
+	return topValue(token.LT, args)
+}
+
+// -----------------------------------------------------------------------------
+
+var fnsMath1 = [...]math1{
+	{"abs", math.Abs},
+	{"acos", math.Acos},
+	{"acosh", math.Acosh},
+	{"asin", math.Asin},
+	{"asinh", math.Asinh},
+	{"atan", math.Atan},
+	{"atanh", math.Atanh},
+	{"ceil", math.Ceil},
+	{"cos", math.Cos},
+	{"cosh", math.Cosh},
+	{"erf", math.Erf},
+	{"erfc", math.Erfc},
+	{"exp", math.Exp},
+	{"expm1", math.Expm1},
+	{"floor", math.Floor},
+	{"gamma", math.Gamma},
+	{"j0", math.J0},
+	{"j1", math.J1},
+	{"log", math.Log},
+	{"log10", math.Log10},
+	{"log1p", math.Log1p},
+	{"log2", math.Log2},
+	{"round", math.Round},
+	{"roundToEven", math.RoundToEven},
+	{"sin", math.Sin},
+	{"sinh", math.Sinh},
+	{"sqrt", math.Sqrt},
+	{"tan", math.Tan},
+	{"tanh", math.Tanh},
+	{"trunc", math.Trunc},
+	{"y0", math.Y0},
+	{"y1", math.Y1},
+}
+
+var fnsMath2 = [...]math2{
+	{"atan2", math.Atan2},
+	{"copysign", math.Copysign},
+	{"dim", math.Dim},
+	{"hypot", math.Hypot},
+	{"mod", math.Mod},
+	{"nextafter", math.Nextafter},
+	{"pow", math.Pow},
+	{"remainder", math.Remainder},
+}
+
+var fnsMathIntFloat = [...]mathIntFloat{
+	{"jn", math.Jn},
+	{"yn", math.Yn},
+}
+
+var fnsNamed = [...]namedFn{
+	{"max", Max},
+	{"min", Min},
+}
+
+func init() {
+	mod := variant.NewModule("math")
+	for _, m := range fnsMath1 {
+		regMath1(mod, m.name, m.fn)
+	}
+	for _, m := range fnsMath2 {
+		regMath2(mod, m.name, m.fn)
+	}
+	for _, m := range fnsMathIntFloat {
+		regMathIntFloat(mod, m.name, m.fn)
+	}
+	for _, m := range fnsNamed {
+		mod.Insert(m.name, m.fn)
+	}
+}
+
+// -----------------------------------------------------------------------------
diff --git a/tpl/variant/module.go b/tpl/variant/module.go
new file mode 100644
index 00000000..4243e1df
--- /dev/null
+++ b/tpl/variant/module.go
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package variant
+
+// -----------------------------------------------------------------------------
+
+type Module struct {
+	Name string
+	objs map[string]any
+}
+
+func NewModule(name string) *Module {
+	mod := &Module{
+		Name: name,
+		objs: make(map[string]any),
+	}
+	universe.Insert(name, mod)
+	return mod
+}
+
+func (p *Module) Lookup(name string) (v any, ok bool) {
+	v, ok = p.objs[name]
+	return
+}
+
+func (p *Module) Insert(name string, v any) {
+	objs := p.objs
+	if _, ok := objs[name]; ok {
+		panic("object exists: " + name)
+	}
+	objs[name] = v
+}
+
+func (p *Module) Merge(mod *Module) {
+	for name, obj := range mod.objs {
+		p.Insert(name, obj)
+	}
+}
+
+// -----------------------------------------------------------------------------
+
+var (
+	universe = &Module{
+		objs: map[string]any{
+			"int": CastInt,
+		},
+	}
+)
+
+func CastInt(args ...any) any {
+	if len(args) != 1 {
+		panic("int: arity mismatch")
+	}
+	switch v := Eval(args[0]).(type) {
+	case float64:
+		return int(v)
+	case int:
+		return v
+	}
+	panic("can't convert to int")
+}
+
+// Call calls a function.
+func Call(name string, args ...any) any {
+	if o, ok := universe.objs[name]; ok {
+		if fn, ok := o.(func(...any) any); ok {
+			return fn(args...)
+		}
+		panic("not a function: " + name)
+	}
+	panic("function not found: " + name)
+}
+
+// DelayCall delays to call a function.
+func DelayCall(name string, args ...any) any {
+	return func() any {
+		return Call(name, args...)
+	}
+}
+
+// -----------------------------------------------------------------------------
+
+func InitUniverse(names ...string) {
+	for _, name := range names {
+		if o, ok := universe.objs[name]; ok {
+			if mod, ok := o.(*Module); ok {
+				universe.Merge(mod)
+			} else {
+				panic("not a module: " + name)
+			}
+		} else {
+			panic("module not found: " + name)
+		}
+	}
+}
+
+// -----------------------------------------------------------------------------
diff --git a/tpl/variant/time/time.go b/tpl/variant/time/time.go
new file mode 100644
index 00000000..2831afa6
--- /dev/null
+++ b/tpl/variant/time/time.go
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package time
+
+import (
+	"time"
+
+	"github.com/goplus/gop/tpl/variant"
+)
+
+// -----------------------------------------------------------------------------
+
+func now(args ...any) any {
+	if len(args) > 0 {
+		panic("function call: arity mismatch")
+	}
+	return time.Now()
+}
+
+func init() {
+	mod := variant.NewModule("time")
+	mod.Insert("now", now)
+}
+
+// -----------------------------------------------------------------------------
diff --git a/tpl/variant/variant.go b/tpl/variant/variant.go
index ee380415..7372195f 100644
--- a/tpl/variant/variant.go
+++ b/tpl/variant/variant.go
@@ -25,11 +25,6 @@ import (
 // DelayValue represents a delayed value.
 type DelayValue = func() any
 
-// Delay delays a value.
-func Delay(getter DelayValue) any {
-	return getter
-}
-
 // Eval evaluates a value.
 func Eval(v any) any {
 	if d, ok := v.(DelayValue); ok {
@@ -38,6 +33,26 @@ func Eval(v any) any {
 	return v
 }
 
+// Float converts a value to float64.
+func Float(v any) float64 {
+	switch v := Eval(v).(type) {
+	case int:
+		return float64(v)
+	case float64:
+		return v
+	}
+	panic("can't convert to float")
+}
+
+// Int ensures a value is int.
+// It doesn't convert float to int.
+func Int(v any) int {
+	if v, ok := Eval(v).(int); ok {
+		return v
+	}
+	panic("not an int")
+}
+
 // -----------------------------------------------------------------------------
 
 func cmpInt(op token.Token, x, y int) bool {


Test patch:
diff --git a/cl/_testgop/domaintpl/in.gop b/cl/_testgop/domaintpl/in.gop
index fb1c8a05..9fdf10c6 100644
--- a/cl/_testgop/domaintpl/in.gop
+++ b/cl/_testgop/domaintpl/in.gop
@@ -1,10 +1,10 @@
 tpl`
 file = stmts => {
-	return this
+	return self
 }
 
 stmts = *(stmt ";") => {
-	return [n.([]any)[0] for n in this]
+	return [n.([]any)[0] for n in self]
 }
 
 stmt = varStmt | constStmt | outputStmt | inputStmt | ifStmt | whileStmt | untilStmt | assignStmt
diff --git a/cl/_testgop/domaintpl/out.go b/cl/_testgop/domaintpl/out.go
index 4871fe43..094a464a 100644
--- a/cl/_testgop/domaintpl/out.go
+++ b/cl/_testgop/domaintpl/out.go
@@ -5,11 +5,11 @@ import "github.com/goplus/gop/tpl"
 func main() {
 	tpl.New(`
 file = stmts => {
-	return this
+	return self
 }
 
 stmts = *(stmt ";") => {
-	return [n.([]any)[0] for n in this]
+	return [n.([]any)[0] for n in self]
 }
 
 stmt = varStmt | constStmt | outputStmt | inputStmt | ifStmt | whileStmt | untilStmt | assignStmt
@@ -51,11 +51,11 @@ parenExpr = "(" expr ")"
 exprlist = expr % ","
 
 namelist = IDENT % ","
-`, "file", func(this interface{}) interface{} {
-		return this
-	}, "stmts", func(this []interface{}) interface{} {
+`, "file", func(self interface{}) interface{} {
+		return self
+	}, "stmts", func(self []interface{}) interface{} {
 		return func() (_gop_ret []interface{}) {
-			for _, n := range this {
+			for _, n := range self {
 				_gop_ret = append(_gop_ret, n.([]interface{})[0])
 			}
 			return