Repository: goplus/gop
Pull request #2242 (2025-04-01 06:10:44)

minispec


Patch:
diff --git a/.github/codecov.yml b/.github/codecov.yml
index 043ffcbd..cdeecc34 100644
--- a/.github/codecov.yml
+++ b/.github/codecov.yml
@@ -7,6 +7,7 @@ coverage:
     - "format"
     - "demo"
     - "demo/unit-test"
+    - "doc/spec"
     - "cmd"
     - "cl/cltest"
     - "cl/outline"
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index da72df3b..074b7b7c 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -52,3 +52,41 @@ jobs:
       uses: codecov/codecov-action@v5
       with:
         slug: goplus/gop
+
+  TestTypeAlias:
+    strategy:
+      matrix:
+        os:
+        - ubuntu-latest
+        - windows-latest
+        - macos-latest
+        go:
+        - 1.23.x
+        - 1.24.x
+    runs-on: ${{ matrix.os }}
+    steps:
+    - uses: actions/checkout@v4
+
+    - name: Set up Go
+      uses: actions/setup-go@v5
+      with:
+        go-version: ${{ matrix.go }}
+
+    - name: Update golang.org/x/tools
+      run: |
+        go work init .
+        go work edit -replace golang.org/x/tools=golang.org/x/tools@v0.30
+
+    - name: Test Go+ installer
+      run: |
+        git config --global user.email "build-robot@goplus.org"
+        git config --global user.name "build robot"
+        go test -v cmd/make_test.go
+
+    - name: Run testcases
+      run: go test -v -coverprofile="coverage.txt" -covermode=atomic ./...
+
+    - name: Codecov
+      uses: codecov/codecov-action@v5
+      with:
+        slug: goplus/gop
diff --git a/VERSION b/VERSION
deleted file mode 100644
index f995ee2e..00000000
--- a/VERSION
+++ /dev/null
@@ -1,1 +0,0 @@
-v1.3.7
diff --git a/ast/ast_gop.go b/ast/ast_gop.go
index 7d1d0024..8703c101 100644
--- a/ast/ast_gop.go
+++ b/ast/ast_gop.go
@@ -29,7 +29,7 @@ import (
 //
 // here overloadFunc represents
 //
-// `func(params) {...}`
+// `func(params) results {...}`
 // `funcName`
 // `(*T).methodName`
 type OverloadFuncDecl struct {
diff --git a/cl/builtin.go b/cl/builtin.go
index 93445a39..6becb512 100644
--- a/cl/builtin.go
+++ b/cl/builtin.go
@@ -91,6 +91,11 @@ func newBuiltinDefault(pkg *gogen.Package, conf *gogen.Config) *types.Package {
 	pkg.TryImport("strings")
 	if ng.Types != nil {
 		initMathBig(pkg, conf, ng)
+		if obj := ng.Types.Scope().Lookup("Gop_ninteger"); obj != nil {
+			if _, ok := obj.Type().(*types.Basic); !ok {
+				conf.EnableTypesalias = true
+			}
+		}
 	}
 	initBuiltin(pkg, builtin, os, fmt, ng, iox, buil, reflect)
 	gogen.InitBuiltin(pkg, builtin, conf)
diff --git a/cl/compile.go b/cl/compile.go
index 09f3acb2..cc2794fa 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -1040,7 +1040,12 @@ func preloadFile(p *gogen.Package, ctx *blockCtx, f *ast.File, goFile string, ge
 								if debugLoad {
 									log.Println("==> Load > AliasType", name)
 								}
-								defs.AliasType(name, toType(ctx, t.Type), tName)
+								typ := defs.AliasType(name, toType(ctx, t.Type), tName)
+								if rec := ctx.recorder(); rec != nil {
+									if obj, ok := typ.(interface{ Obj() *types.TypeName }); ok {
+										rec.Def(tName, obj.Obj())
+									}
+								}
 								return
 							}
 							if debugLoad {
diff --git a/cl/expr.go b/cl/expr.go
index 1bd0b6b3..68cf1c52 100644
--- a/cl/expr.go
+++ b/cl/expr.go
@@ -1111,11 +1111,14 @@ func compileDomainTextLit(ctx *blockCtx, v *ast.DomainTextLit) {
 		*/
 	}
 
-	cb.Val(imp.Ref("New")).
-		Val(&goast.BasicLit{Kind: gotoken.STRING, Value: v.Value}, v)
-
 	n := 1
 	if path == tplPkgPath {
+		pos := ctx.fset.Position(v.ValuePos)
+		filename := relFile(ctx.relBaseDir, pos.Filename)
+		cb.Val(imp.Ref("NewEx")).
+			Val(&goast.BasicLit{Kind: gotoken.STRING, Value: v.Value}, v).
+			Val(filename).Val(pos.Line).Val(pos.Column)
+		n += 3
 		if f, ok := v.Extra.(*tpl.File); ok {
 			decls := f.Decls
 			for _, decl := range decls {
@@ -1129,6 +1132,9 @@ func compileDomainTextLit(ctx *blockCtx, v *ast.DomainTextLit) {
 				}
 			}
 		}
+	} else {
+		cb.Val(imp.Ref("New")).
+			Val(&goast.BasicLit{Kind: gotoken.STRING, Value: v.Value}, v)
 	}
 	cb.CallWith(n, 0, v)
 }
diff --git a/cmd/chore/gopminispec/minispec.gop b/cmd/chore/gopminispec/minispec.gop
new file mode 100644
index 00000000..a674a437
--- /dev/null
+++ b/cmd/chore/gopminispec/minispec.gop
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+import (
+	"flag"
+	"gop/doc/spec/mini"
+	"gop/tpl"
+	"gop/tpl/matcher"
+	"gop/x/gopprojs"
+	"os"
+	"path/filepath"
+)
+
+func isGopFile(file string) bool {
+	if len(file) > 4 {
+		switch file[len(file)-4:] {
+		case ".gop", ".gox", ".gsh", ".spx", ".yap":
+			return true
+		}
+	}
+	return false
+}
+
+func parseFiles(files []string, dumpAST bool) (nErr int) {
+	for file in files {
+		fprintln os.Stderr, "\n==> Parsing ${file}"
+		f, err := mini.parseFile(nil, file, nil)
+		if err != nil {
+			fprintln os.Stderr, err
+			nErr++
+		} else if dumpAST {
+			tpl.dump f
+		}
+	}
+	return
+}
+
+func parseDir(dir string, recursively, dumpAST bool) (nErr int) {
+	filepath.walkDir dir, (file, d, err) => {
+		if err != nil {
+			return err
+		} else if fname := d.name; d.isDir {
+			if recursively {
+				if fname.hasPrefix("_") || fname == "fullspec" {
+					return filepath.SkipDir
+				}
+			} else if file != dir {
+				return filepath.SkipDir
+			}
+		} else if !fname.hasPrefix("_") && isGopFile(fname) {
+			fprintln os.Stderr, "\n==> Parsing ${file}"
+			f, err := mini.parseFile(nil, file, nil)
+			if err != nil {
+				fprintln os.Stderr, err
+				nErr++
+			} else if dumpAST {
+				tpl.dump f
+			}
+		}
+		return nil
+	}
+	return
+}
+
+var (
+	flagVerbose = flag.Bool("v", false, "print verbose information")
+	flagDumpAST = flag.Bool("ast", false, "dump AST")
+)
+
+flag.parse
+
+pattern := flag.args
+if pattern.len == 0 {
+	echo "Usage: gopminispec [-v] packages"
+	return
+}
+
+if *flagVerbose {
+	matcher.setDebug matcher.DbgFlagAll
+}
+
+dumpAST := *flagDumpAST
+nErr := 0
+
+projs, err := gopprojs.parseAll(pattern...)
+if err != nil {
+	fprintln os.Stderr, err
+	os.exit 1
+}
+
+for proj in projs {
+	switch v := proj.(type) {
+	case *gopprojs.DirProj:
+		dir := v.Dir
+		recursively := dir.hasSuffix("/...")
+		if recursively {
+			dir = dir[:len(dir)-4]
+		}
+		nErr += parseDir(dir, recursively, dumpAST)
+	case *gopprojs.FilesProj:
+		nErr += parseFiles(v.Files, dumpAST)
+	default:
+		panic "only support local files and directories"
+	}
+}
+fprintln os.Stderr
+
+if nErr > 0 {
+	os.exit 1
+}
diff --git a/demo/mixgo-complex/bar.go b/demo/fullspec/mixgo-complex/bar.go
similarity index 100%
rename from demo/mixgo-complex/bar.go
rename to demo/fullspec/mixgo-complex/bar.go
diff --git a/demo/mixgo-complex/foo.gop b/demo/fullspec/mixgo-complex/foo.gop
similarity index 100%
rename from demo/mixgo-complex/foo.gop
rename to demo/fullspec/mixgo-complex/foo.gop
diff --git a/demo/overloadfunc1/add.gop b/demo/fullspec/overloadfunc1/add.gop
similarity index 100%
rename from demo/overloadfunc1/add.gop
rename to demo/fullspec/overloadfunc1/add.gop
diff --git a/demo/overloadfunc2/mul.gop b/demo/fullspec/overloadfunc2/mul.gop
similarity index 100%
rename from demo/overloadfunc2/mul.gop
rename to demo/fullspec/overloadfunc2/mul.gop
diff --git a/demo/overloadmethod/method.gop b/demo/fullspec/overloadmethod/method.gop
similarity index 100%
rename from demo/overloadmethod/method.gop
rename to demo/fullspec/overloadmethod/method.gop
diff --git a/demo/overloadop1/overloadop.gop b/demo/fullspec/overloadop1/overloadop.gop
similarity index 100%
rename from demo/overloadop1/overloadop.gop
rename to demo/fullspec/overloadop1/overloadop.gop
diff --git a/demo/overloadop2/overloadop.gop b/demo/fullspec/overloadop2/overloadop.gop
similarity index 100%
rename from demo/overloadop2/overloadop.gop
rename to demo/fullspec/overloadop2/overloadop.gop
diff --git a/demo/gop-parser/gopcalc/calc.gop b/demo/gop-calc/calc.gop
similarity index 97%
rename from demo/gop-parser/gopcalc/calc.gop
rename to demo/gop-calc/calc.gop
index d9bd0cc8..59b28311 100644
--- a/demo/gop-parser/gopcalc/calc.gop
+++ b/demo/gop-calc/calc.gop
@@ -42,7 +42,7 @@ func calc(e ast.Expr) float64 {
 }
 
 print "> "
-for line <- os.Stdin {
+for line in os.Stdin {
 	e, err := parser.parseExpr(line)
 	if err != nil {
 		print "error: ${err}\n> "
diff --git a/demo/gop-parser/_spec/tpl.gop b/demo/gop-parser/_spec/tpl.gop
deleted file mode 100644
index 16ce1b57..00000000
--- a/demo/gop-parser/_spec/tpl.gop
+++ /dev/null
@@ -1,9 +0,0 @@
-doc = +rule
-
-rule = IDENT '=' expr ';'
-
-expr = +term % '|'
-
-term = factor % '%'
-
-factor = IDENT | CHAR | STRING | ('*' | '+' | '?') factor | '(' expr ')'
diff --git a/demo/gop-scanner/rpncalc/rpncalc.gop b/demo/gop-scanner/rpncalc/rpncalc.gop
index 37fd9abf..3e61e38b 100644
--- a/demo/gop-scanner/rpncalc/rpncalc.gop
+++ b/demo/gop-scanner/rpncalc/rpncalc.gop
@@ -34,6 +34,6 @@ func calc(expr string) string {
 }
 
 print "> "
-for line <- os.Stdin {
+for line in os.Stdin {
 	print "${calc(line)}\n> "
 }
diff --git a/demo/gop-scanner/simplecalc/calc.gop b/demo/gop-scanner/simplecalc/calc.gop
index 272d5834..74a11596 100644
--- a/demo/gop-scanner/simplecalc/calc.gop
+++ b/demo/gop-scanner/simplecalc/calc.gop
@@ -69,6 +69,6 @@ func calc(expr string) string {
 }
 
 print "> "
-for line <- os.Stdin {
+for line in os.Stdin {
 	print "${calc(line)}\n> "
 }
diff --git a/demo/gsh-exec/exec.gsh b/demo/gsh-exec/exec.gsh
index 7937fa30..d33cd5b0 100644
--- a/demo/gsh-exec/exec.gsh
+++ b/demo/gsh-exec/exec.gsh
@@ -4,4 +4,4 @@ exec "FOO=100 gop run ./foo"
 exec {"FOO": "101"}, "gop", "run", "./foo"
 exec "gop", "run", "./foo"
 exec "ls $HOME"
-ls "${HOME}"
+ls ${HOME}
diff --git a/demo/lambda1/lambda.gop b/demo/lambda1/lambda.gop
new file mode 100644
index 00000000..28f88848
--- /dev/null
+++ b/demo/lambda1/lambda.gop
@@ -0,0 +1,8 @@
+func f(x float64, t func(float64) float64) float64 {
+    return t(x)
+}
+
+echo f(1.0, x => 2 * x)
+echo f(5.0, (x) => {
+    return 2 * x
+})
diff --git a/demo/tpl-calc-dump/calc_dump.gop b/demo/tpl-calc-dump/calc_dump.gop
index e473d8e9..fcb7c676 100644
--- a/demo/tpl-calc-dump/calc_dump.gop
+++ b/demo/tpl-calc-dump/calc_dump.gop
@@ -14,7 +14,7 @@ operand = INT | FLOAT | "(" expr ")"
 `!
 
 print "> "
-for line <- os.Stdin {
+for line in os.Stdin {
 	e, err := cl.parseExpr(line, nil)
 	if err != nil {
 		print "${err}\n> "
diff --git a/demo/tpl-natural-lang/nlang.gop b/demo/tpl-natural-lang/nlang.gop
index f61708b7..30bea09b 100644
--- a/demo/tpl-natural-lang/nlang.gop
+++ b/demo/tpl-natural-lang/nlang.gop
@@ -8,7 +8,7 @@ verb = "attack" | "love" | "eat" | "hate"
 `!
 
 print "> "
-for line <- os.Stdin {
+for line in os.Stdin {
 	e, err := cl.parseExpr(line, nil)
 	if err != nil {
 		print "${err}\n> "
diff --git a/demo/tpl-pseudo/pseudo.gox b/demo/tpl-pseudo/pseudo.gox
index fe8a54e1..f0d056bf 100644
--- a/demo/tpl-pseudo/pseudo.gox
+++ b/demo/tpl-pseudo/pseudo.gox
@@ -38,7 +38,7 @@ func setValue(name string, v any) {
 		panic "variable ${name} is undefined"
 	}
 	if reflect.typeOf(oldv) != reflect.typeOf(v) {
-		panic("assignment of ${name}: type mismatch")
+		panic "assignment of ${name}: type mismatch"
 	}
 	vars[name] = v
 }
@@ -97,14 +97,7 @@ constStmt = "CONSTANT" IDENT "<-" expr => {
 
 assignStmt = IDENT "<-" expr => {
 	t := self[0].(*tpl.Token)
-	return delay.setValue(t.Lit, self[2], setValue)
-}
-
-outputStmt = "OUTPUT" (expr % ",") => {
-	exprlist := self[1].([]any)
-	return delay.listOp(exprlist, vals => {
-		echo vals...
-	})
+	return delay.setValue(t.Lit, setValue, self[2])
 }
 
 inputStmt = "INPUT" IDENT => {
@@ -116,6 +109,13 @@ inputStmt = "INPUT" IDENT => {
 	})
 }
 
+outputStmt = "OUTPUT" (expr % ",") => {
+	exprlist := self[1].([]any)
+	return delay.listOp(exprlist, vals => {
+		echo vals...
+	})
+}
+
 ifStmt = "IF" expr "THEN" ";" stmts ?("ELSE" ";" stmts) "ENDIF" => {
 	return delay.ifElse(self[1], self[4], self[5], 2)
 }
@@ -146,8 +146,8 @@ boolean = "BOOLEAN" => {
 	return false
 }
 
-expr = cmpExpr % (and | or) => {
-	return tpl.binaryOp(false, self, (op, x, y) => {
+expr = cmpExpr % and % or => {
+	return tpl.binaryOp(true, self, (op, x, y) => {
 		return delay.logicOp(op.Tok, x, y)
 	})
 }
diff --git a/demo/tpl-vcalc/variant_calc.gop b/demo/tpl-vcalc/variant_calc.gop
index 4ba10050..a41eb103 100644
--- a/demo/tpl-vcalc/variant_calc.gop
+++ b/demo/tpl-vcalc/variant_calc.gop
@@ -11,17 +11,17 @@ import (
 
 cl := tpl`
 
-expr = cmpExpr % ("||" | "&&" | and | or) => {
-	return tpl.binaryOp(false, self, (op, x, y) => {
+expr = cmpExpr % and % or => {
+	return tpl.binaryOp(true, self, (op, x, y) => {
 		return variant.logicOp(op.Tok, x, y)
 	})
 }
 
-and = "and" | "AND" => {
+and = "and" | "AND" | "&&" => {
 	return &tpl.Token{Tok: token.LAND}
 }
 
-or = "or" | "OR" => {
+or = "or" | "OR" | "||" => {
 	return &tpl.Token{Tok: token.LOR}
 }
 
diff --git a/doc/_spec/go.gop b/doc/_spec/go.gop
deleted file mode 100644
index 6b270105..00000000
--- a/doc/_spec/go.gop
+++ /dev/null
@@ -1,187 +0,0 @@
-// -----------------------------------------------------------------
-
-node Decl {
-	declNode()
-}
-
-node Spec {
-	specNode()
-}
-
-node Stmt {
-	stmtNode()
-}
-
-node Expr {
-	exprNode()
-}
-
-node GenDecl : Decl {
-	TokPos pos
-	Tok token
-	Lparen pos
-	Specs []Spec
-	Rparen pos
-}
-
-node ImportSpec : Spec {
-	Name Expr
-	Path BasicLit
-	EndPos pos
-}
-
-// -----------------------------------------------------------------
-
-File = (pkg:"package" Name:Ident ";") Decls:(*(importDecl ";") *(toplevelDecl ";")) {
-	Package: pkg.Pos
-}
-
-toplevelDecl = valueDecl | typeDecl | FuncDecl
-
-// -----------------------------------------------------------------
-
-importDecl = i:"import" Specs:(ImportSpec | lp:"(" *(ImportSpec ";") rp:")") as GenDecl {
-	TokPos: i.Pos
-	Tok: i.Tok
-	Lparen: lp.Pos
-	Rparen: rp.Pos
-}
-
-ImportSpec = ?Name:(dotPkgName | Ident) Path:stringLit {
-	EndPos: Path.End
-}
-
-dotPkgName = dot:"." as Ident {
-	NamePos: dot.Pos
-	Name: "."
-}
-
-stringLit = s:STRING as BasicLit {
-	ValuePos: s.Pos
-	Kind: s.Tok
-	Value: s.Lit
-}
-
-// -----------------------------------------------------------------
-
-valueDecl = valKw Specs:(ValueSpec | lp:"(" *(ValueSpec ";") rp:")") as GenDecl {
-	TokPos: valKw.Pos
-	Tok: valKw.Tok
-	Lparen: lp.Pos
-	Rparen: rp.Pos
-}
-
-valKw = "const" | "var"
-
-ValueSpec = Names:identList ?Type ?Values:exprList
-
-identList = Ident % ","
-
-Ident = i:IDENT {
-	NamePos: i.Pos
-	Name: i.Lit
-}
-
-// -----------------------------------------------------------------
-
-Type = typeNameEx | typeLit | "(" Type ")" as Expr
-
-typeLit = ArrayType | StructType | pointerType | FuncType | InterfaceType | MapType | ChanType
-
-typeNameEx = typeName ?(lp:"[" Indices:typeList rp:"]" as IndexListExpr {
-	X: typeName
-	Lbrack: lp.Pos
-	Rbrack: rp.Pos
-})
-
-typeName = Ident | qualifiedIdent as Expr
-
-typeList = Type % ","
-
-qualifiedIdent = X:Ident "." Sel:Ident as SelectorExpr
-
-// -----------------------------------------------------------------
-
-ArrayType = lp:"[" ?Len:expr "]" Elt:Type {
-	Lbrack: lp.Pos
-}
-
-StructType = "struct" Fields:structBody {
-	Struct: struct.Pos
-}
-
-structBody = lp:"{" List:(structField % ";") ?";" rp:"}" as FieldList {
-	Opening: lp.Pos
-	Closing: rp.Pos
-}
-
-structField = (Names:identList Type | Type:embeddedField) ?Tag:stringLit as Field
-
-embeddedField = typeNameEx | (op:"*" X:typeNameEx as StarExpr {
-	Star: op.Pos
-})
-
-// -----------------------------------------------------------------
-
-AssignStmt = Lhs:exprList assignOp Rhs:exprList {
-	TokPos: assignOp.Pos
-	Tok: assignOp.Tok
-}
-
-exprList = expr % ","
-
-assignOp = ":=" | "=" | "+=" | "-=" | "*=" | "/=" | "%="
-
-// -----------------------------------------------------------------
-
-expr = unaryExpr | BinaryExpr
-
-BinaryExpr = X:expr binOp Y:expr { // TODO: precedence
-	OpPos: binOp.Pos
-	Op: binOp.Tok
-}
-
-binOp = "||" | "&&" | relOp | addOp | mulOp
-
-relOp = "==" | "!=" | "<" | "<=" | ">" | ">="
-
-addOp = "+" | "-" | "|" | "^"
-
-mulOp = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^"
-
-unaryExpr = primaryExpr | (unaryOp X:unaryExpr as UnaryExpr {
-	OpPos: unaryOp.Pos
-	Op: unaryOp.Tok
-})
-
-unaryOp = "+" | "-" | "!" | "^" | "*" | "&" | "<-"
-
-primaryExpr = operand | CallExpr | SelectorExpr | IndexExpr | SliceExpr | TypeAssertExpr
-
-operand = TODO
-
-CallExpr = Fun:primaryExpr lp:"(" Args:exprList ?el:"..." ?"," rp:")" {
-	Lparen: lp.Pos
-	Rparen: rp.Pos
-	Ellipsis: el != nil
-}
-
-SelectorExpr = X:primaryExpr "." Sel:Ident
-
-IndexExpr = X:primaryExpr lp:"[" Index:expr ?"," rp:"]" {
-	Lbrack: lp.Pos
-	Rbrack: rp.Pos
-}
-
-SliceExpr = X:primaryExpr lp:"[" ?Low:expr ":" ?High:expr ?(":" Max:expr) rp:"]" {
-	Lbrack: lp.Pos
-	Rbrack: rp.Pos
-	Slice3: Max != nil
-}
-
-TypeAssertExpr = X:primaryExpr "." lp:"(" Type rp:")" {
-	Lparen: lp.Pos
-	Rparen: rp.Pos
-}
-
-// -----------------------------------------------------------------
diff --git a/doc/spec/mini/mini.gop b/doc/spec/mini/mini.gop
new file mode 100644
index 00000000..cb95d41f
--- /dev/null
+++ b/doc/spec/mini/mini.gop
@@ -0,0 +1,225 @@
+/*
+ * 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 mini
+
+import (
+	"github.com/goplus/gop/token"
+	"github.com/goplus/gop/tpl"
+)
+
+const (
+	domianTextQuote  = '`'
+	domianTextExpect = "expect domainTag`...`"
+)
+
+var _off = tpl.showConflict(false)
+
+var Spec = tpl`
+
+SourceFile = ?PackageClause *(ImportDecl ";") *(TopLevelDecl ";") ?MainStmts
+
+PackageClause = "package" IDENT ";"
+
+ImportDecl = "import" (ImportSpec | "(" *(ImportSpec ";") ")")
+
+ImportSpec = ?(IDENT | ".") STRING
+
+TopLevelDecl = Declaration | FuncDecl
+
+Declaration = ConstDecl | VarDecl | TypeDecl
+
+ConstDecl = "const" (ConstSpec | "(" *(ConstSpec ";") ")")
+
+ConstSpec = IdentifierList ?Type ?("=" ExpressionList)
+
+IdentifierList = IDENT % ","
+
+LambdaExprList = LambdaExpr % ","
+
+ExpressionList = Expression % ","
+
+VarDecl = "var" (VarSpec | "(" *(VarSpec ";") ")")
+
+VarSpec = IdentifierList ("=" ExpressionList | Type ?("=" LambdaExprList))
+
+TypeDecl = "type" (TypeSpec | "(" *(TypeSpec ";") ")")
+
+TypeSpec = IDENT ?"=" Type
+
+FuncDecl = "func" IDENT Signature ?Block
+
+Signature = Parameters ?Result
+
+Parameters = "(" ?(ParameterList ?",") ")"
+
+ParameterList = IdentifierList ?(Type *("," IdentifierList Type) | *("," Type))
+
+Result = Parameters | NoParenType
+
+MainStmts = StatementList
+
+// -----------------------------------------------------------------
+
+Block = "{" StatementList "}"
+
+StatementList = *(Statement ";")
+
+Statement =
+	Declaration | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
+	FallthroughStmt | IfStmt | SwitchStmt | ForStmt | DeferStmt | Block |
+	LabeledStmt | CommandStmt | SimpleStmt
+
+ReturnStmt = "return" ?LambdaExprList
+
+BreakStmt = "break" ?IDENT
+
+ContinueStmt = "continue" ?IDENT
+
+GotoStmt = "goto" IDENT
+
+FallthroughStmt = "fallthrough"
+
+IfStmt = "if" ?(SimpleStmt ";") Expression Block ?("else" (IfStmt | Block))
+
+SwitchStmt = "switch" ?(SimpleStmt ";") SwitchGuard "{" *CaseClause "}"
+
+SwitchGuard = ?(IDENT ":=") PrimaryExpr | ?Expression
+
+CaseClause = ("case" ExpressionList | "default") ":" StatementList
+
+ForStmt = "for" (IDENT ?("," IDENT) "in" RangeExpr | ?RangeExpr) Block
+
+DeferStmt = "defer" Expression
+
+LabeledStmt = IDENT ":" Statement
+
+SimpleStmt = SendStmt | ShortVarDecl | IncDecStmt | Assignment | ExpressionStmt | EmptyStmt
+
+SendStmt = IDENT ?("." IDENT) "<-" LambdaExprList
+
+ShortVarDecl = IdentifierList ":=" ExpressionList
+
+IncDecStmt = Expression ("++" | "--")
+
+Assignment = ExpressionList (
+	"=" | "+=" | "-=" | "|=" | "^=" |
+	"*=" | "/=" | "%=" | "<<=" | ">>=" | "&=" | "&^=") LambdaExprList
+
+ExpressionStmt = Expression
+
+CommandStmt = IDENT ?("." IDENT) SPACE LambdaExprList
+
+EmptyStmt = ""
+
+// -----------------------------------------------------------------
+
+NoParenType = TypeLit | TypeName
+
+Type = TypeLit | TypeName | "(" Type ")"
+
+TypeName = IDENT ?("." IDENT)
+
+TypeLit = PointerType | ArrayType | MapType | FuncType | StructType | InterfaceType
+
+PointerType = "*" Type
+
+ArrayType = "[" ?Expression "]" Type
+
+MapType = "map" "[" Type "]" Type
+
+FuncType = "func" Signature
+
+StructType = "struct" "{" *(FieldDecl ";") "}"
+
+FieldDecl = ("*" TypeName | FieldsOrNonPtrEmbeddedField) ?Tag
+
+FieldsOrNonPtrEmbeddedField = IDENT ("." IDENT | +("," IDENT) Type | ?Type)
+
+Tag = STRING
+
+InterfaceType = "interface" "{" *(InterfaceElem ";") "}"
+
+InterfaceElem = MethodElem | TypeName
+
+MethodElem = IDENT Signature
+
+// -----------------------------------------------------------------
+
+LambdaExpr = ("(" ?(IDENT % ",") ")" | ?IDENT) "=>" LambdaBody | Expression
+
+LambdaBody = Block | "(" LambdaExpr % "," ")" | LambdaExpr
+
+RangeExpr = rangeExprEnd | Expression ?rangeExprEnd
+
+rangeExprEnd = ":" Expression ?(":" ?Expression)
+
+Expression = cmpExpr % "&&" % "||"
+
+cmpExpr = mathExpr % ("==" | "!=" | "<" | "<=" | ">" | ">=")
+
+mathExpr = UnaryExpr % ("*" | "/" | "%" | "<<" | ">>" | "&" | "&^") % ("+" | "-" | "|" | "^")
+
+UnaryExpr = PrimaryExpr | ("-" | "!" | "^" | "*" | "&" | "+") UnaryExpr
+
+PrimaryExpr = Operand *(
+	CallOrConversion | SelectorOrTypeAssertion | IndexOrSlice | ErrWrap)
+
+Operand =
+	INT ?UNIT | FLOAT ?UNIT | STRING | CHAR | RAT | IMAG | "(" LambdaExpr ")" |
+	LiteralValue | CompositeLit | FunctionLit | Env | "c" ++ QSTRING | "py" ++ QSTRING |
+	ListCompositeLit | DomainTextLit | NamedCompositeLit | IDENT
+
+Env = "$" ("{" IDENT "}" | IDENT)
+
+DomainTextLit = IDENT ++ RAWSTRING
+
+NamedCompositeLit = TypeName ++ "{" ElementList "}"
+
+CompositeLit = (MapType | StructType) LiteralValue
+
+ListCompositeLit = LBRACK ?("..." | LambdaExpr % ",") (RBRACK ++ Type LiteralValue | RBRACK)
+
+LiteralValue = "{" ElementList "}"
+
+ElementList = ?(KeyedElement % "," ?",")
+
+KeyedElement = ?(Key ":") Element
+
+Key = LiteralValue | Expression
+
+Element = LiteralValue | LambdaExpr
+
+FunctionLit = "func" Signature Block
+
+CallOrConversion = "(" ?(LambdaExpr % ",") ?"..." ?"," ")"
+
+SelectorOrTypeAssertion = "." (IDENT | "(" Type ")")
+
+IndexOrSlice = "[" (":" ?Expression | Expression (":" ?Expression | ?",")) "]"
+
+ErrWrap = "!" | "?" ?(":" UnaryExpr)
+`!
+
+// -----------------------------------------------------------------
+
+func ParseFile(fset *token.FileSet, filename string, src any) (any, error) {
+	return Spec.Parse(filename, src, &tpl.Config{
+		Fset: fset,
+	})
+}
+
+// -----------------------------------------------------------------
diff --git a/parser/iox/io.go b/parser/iox/io.go
index 3aa29653..b26adbb1 100644
--- a/parser/iox/io.go
+++ b/parser/iox/io.go
@@ -21,6 +21,8 @@ import (
 	"errors"
 	"io"
 	"os"
+
+	"github.com/qiniu/x/byteutil"
 )
 
 // -----------------------------------------------------------------------------
@@ -32,7 +34,7 @@ var (
 func ReadSource(src any) ([]byte, error) {
 	switch s := src.(type) {
 	case string:
-		return []byte(s), nil
+		return byteutil.Bytes(s), nil
 	case []byte:
 		return s, nil
 	case *bytes.Buffer:
diff --git a/parser/parser.go b/parser/parser.go
index 5e7ca8fc..0d92069e 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1754,7 +1754,7 @@ func (p *parser) parseOperand(lhs, allowTuple, allowCmd bool) (x ast.Expr, isTup
 	switch p.tok {
 	case token.IDENT:
 		ident := p.parseIdent()
-		if p.tok == token.STRING && p.pos == ident.End() && strings.HasPrefix(p.lit, "`") {
+		if p.tok == token.STRING && p.pos == ident.End() && p.lit[0] == '`' {
 			// domain text: tpl`...`
 			var pos, lit = p.pos, p.lit
 			var extra any
diff --git a/scanner/scanner.go b/scanner/scanner.go
index 962e7150..c004b706 100644
--- a/scanner/scanner.go
+++ b/scanner/scanner.go
@@ -54,8 +54,8 @@ type Scanner struct {
 	rdOffset   int  // reading offset (position after current character)
 	lineOffset int  // current line offset
 	nParen     int
+	unitVal    string
 	insertSemi bool // insert a semicolon before next newline
-	needUnit   bool
 
 	// public state - ok to modify
 	ErrorCount int // number of errors encountered
@@ -503,18 +503,19 @@ func (s *Scanner) scanNumber() (token.Token, string) {
 		s.error(s.offset, "hexadecimal mantissa requires a 'p' exponent")
 	}
 
-	// suffix 'i'
-	if s.ch == 'i' {
-		tok = token.IMAG
-		s.next()
-	} else if s.ch == 'r' {
-		tok = token.RAT
-		s.next()
-	} else if isLetter(s.ch) {
-		s.needUnit = true
+	if isLetter(s.ch) {
+		id := s.scanIdentifier()
+		switch id {
+		case "i":
+			tok = token.IMAG
+		case "r":
+			tok = token.RAT
+		default:
+			s.unitVal = id
+		}
 	}
 
-	lit := string(s.src[offs:s.offset])
+	lit := string(s.src[offs : s.offset-len(s.unitVal)])
 	if tok == token.INT && invalid >= 0 {
 		s.errorf(invalid, "invalid digit %q in %s", lit[invalid-offs], litname(prefix))
 	}
@@ -831,10 +832,11 @@ scanAgain:
 
 	// determine token value
 	insertSemi := false
-	if s.needUnit { // number with unit
+	if s.unitVal != "" { // number with unit
 		insertSemi = true
-		tok, lit = token.UNIT, s.scanIdentifier()
-		s.needUnit = false
+		pos -= token.Pos(len(s.unitVal))
+		tok, lit = token.UNIT, s.unitVal
+		s.unitVal = ""
 		goto done
 	}
 	switch ch := s.ch; {
@@ -937,7 +939,7 @@ scanAgain:
 			if s.ch == '>' { // ->
 				s.next()
 				tok = token.SRARROW
-			} else { // --, -=
+			} else { // -- -=
 				tok = s.switch3(token.SUB, token.SUB_ASSIGN, '-', token.DEC)
 				if tok == token.DEC {
 					insertSemi = true
diff --git a/token/token.go b/token/token.go
index aaa2a07a..631dffb0 100644
--- a/token/token.go
+++ b/token/token.go
@@ -148,7 +148,7 @@ const (
 	additional_literal_end = 97
 
 	ENV  = additional_end2 // ${name}
-	UNIT = additional_end3 // 1m, 2s, 3ms, 4us, 5ns, 6m, 7h, 8d, 9w, 10y
+	UNIT = additional_end3 // 1m, 2.3s, 3ms, 4us, 5ns, 6.5m, 7h, 8d, 9w, 10y
 
 	PYSTRING = additional_literal_beg // py"Hello"
 
@@ -179,6 +179,7 @@ var tokens = [...]string{
 	CSTRING:  "CSTRING",
 	PYSTRING: "PYSTRING",
 	RAT:      "RAT",
+	UNIT:     "UNIT",
 
 	ADD: "+",
 	SUB: "-",
diff --git a/tpl/ast/ast.go b/tpl/ast/ast.go
index 25f87c58..2f70982e 100644
--- a/tpl/ast/ast.go
+++ b/tpl/ast/ast.go
@@ -166,11 +166,11 @@ func (p *UnaryExpr) exprNode()      {}
 
 // -----------------------------------------------------------------------------
 
-// BinaryExpr: R1 % R2
+// BinaryExpr: R1 % R2, R1 ++ R2
 type BinaryExpr struct {
 	X     Expr        // left operand
 	OpPos token.Pos   // operator position
-	Op    token.Token // operator: token.REM (list operator)
+	Op    token.Token // operator: token.REM (list operator), token.INC (adjoin operator)
 	Y     Expr        // right operand
 }
 
diff --git a/tpl/cl/compile.go b/tpl/cl/compile.go
index 812a354a..97d6e2bf 100644
--- a/tpl/cl/compile.go
+++ b/tpl/cl/compile.go
@@ -18,6 +18,7 @@ package cl
 
 import (
 	"fmt"
+	"os"
 	"strconv"
 
 	"github.com/goplus/gop/tpl/ast"
@@ -37,10 +38,16 @@ type Result struct {
 	Rules map[string]*matcher.Var
 }
 
+type choice struct {
+	m *matcher.Choices
+	c *ast.Choice
+}
+
 type context struct {
-	rules map[string]*matcher.Var
-	errs  errors.List
-	fset  *token.FileSet
+	rules   map[string]*matcher.Var
+	choices []choice
+	errs    errors.List
+	fset    *token.FileSet
 }
 
 func (p *context) newErrorf(pos token.Pos, format string, args ...any) error {
@@ -60,8 +67,18 @@ func New(fset *token.FileSet, files ...*ast.File) (ret Result, err error) {
 	return NewEx(nil, fset, files...)
 }
 
+// Config configures the behavior of the compiler.
+type Config struct {
+	RetProcs   map[string]any
+	OnConflict func(fset *token.FileSet, c *ast.Choice, firsts [][]any, i, at int)
+}
+
 // NewEx compiles a set of rules from the given files.
-func NewEx(retProcs map[string]any, fset *token.FileSet, files ...*ast.File) (ret Result, err error) {
+func NewEx(conf *Config, fset *token.FileSet, files ...*ast.File) (ret Result, err error) {
+	if conf == nil {
+		conf = &Config{}
+	}
+	retProcs := conf.RetProcs
 	rules := make(map[string]*matcher.Var)
 	ctx := &context{rules: rules, fset: fset}
 	for _, f := range files {
@@ -107,7 +124,38 @@ func NewEx(retProcs map[string]any, fset *token.FileSet, files ...*ast.File) (re
 		err = ErrNoDocFound
 		return
 	}
-	return Result{doc, rules}, ctx.errs.ToError()
+	defer func() {
+		if e := recover(); e != nil {
+			switch e := e.(type) {
+			case matcher.RecursiveError:
+				ctx.addError(e.Pos, e.Error())
+			default:
+				panic(e)
+			}
+		}
+		err = ctx.errs.ToError()
+	}()
+	onConflict := conf.OnConflict
+	if onConflict == nil {
+		onConflict = onConflictDefault
+	}
+	for _, item := range ctx.choices {
+		item.m.CheckConflicts(func(firsts [][]any, i, at int) {
+			onConflict(fset, item.c, firsts, i, at)
+		})
+	}
+	ret = Result{doc, rules}
+	return
+}
+
+func onConflictDefault(fset *token.FileSet, c *ast.Choice, firsts [][]any, i, at int) {
+	pos := fset.Position(c.Options[i].Pos())
+	LogConflict(pos, firsts, i, at)
+}
+
+// LogConflict logs a conflict between two choices.
+func LogConflict(pos token.Position, firsts [][]any, i, at int) {
+	fmt.Fprintf(os.Stderr, "%v: [WARN] conflict between %v and %v\n", pos, firsts[i], firsts[at])
 }
 
 var (
@@ -120,6 +168,14 @@ var (
 		"IMAG":    token.IMAG,
 		"CHAR":    token.CHAR,
 		"STRING":  token.STRING,
+		"RAT":     token.RAT,
+		"UNIT":    token.UNIT,
+		"LPAREN":  token.LPAREN,
+		"RPAREN":  token.RPAREN,
+		"LBRACK":  token.LBRACK,
+		"RBRACK":  token.RBRACK,
+		"LBRACE":  token.LBRACE,
+		"RBRACE":  token.RBRACE,
 	}
 )
 
@@ -129,11 +185,21 @@ func compileExpr(expr ast.Expr, ctx *context) (matcher.Matcher, bool) {
 		name := expr.Name
 		if v, ok := ctx.rules[name]; ok {
 			return v, true
-		}
-		if tok, ok := idents[name]; ok {
+		} else if tok, ok := idents[name]; ok {
 			return matcher.Token(tok), true
 		}
-		ctx.addErrorf(expr.Pos(), "`%s` is undefined", name)
+		var quoteCh byte
+		switch name {
+		case "RAWSTRING":
+			quoteCh = '`'
+		case "QSTRING":
+			quoteCh = '"'
+		case "SPACE":
+			return matcher.WhiteSpace(), true
+		default:
+			ctx.addErrorf(expr.Pos(), "`%s` is undefined", name)
+		}
+		return matcher.String(quoteCh), true
 	case *ast.BasicLit:
 		lit := expr.Value
 		switch expr.Kind {
@@ -150,10 +216,13 @@ func compileExpr(expr ast.Expr, ctx *context) (matcher.Matcher, bool) {
 			return tokenExpr(token.Token(v), expr, ctx)
 		case token.STRING:
 			v, e := strconv.Unquote(lit)
-			if e != nil || len(v) == 0 {
+			if e != nil {
 				ctx.addError(expr.Pos(), "invalid literal "+lit)
 				break
 			}
+			if v == "" {
+				return matcher.True(), true
+			}
 			if c := v[0]; c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' {
 				return matcher.Literal(token.IDENT, v), true
 			}
@@ -183,7 +252,9 @@ func compileExpr(expr ast.Expr, ctx *context) (matcher.Matcher, bool) {
 				return nil, false
 			}
 		}
-		return matcher.Choice(options...), true
+		ret := matcher.Choice(options...)
+		ctx.choices = append(ctx.choices, choice{ret, expr})
+		return ret, true
 	case *ast.UnaryExpr:
 		if x, ok := compileExpr(expr.X, ctx); ok {
 			switch expr.Op {
@@ -204,6 +275,8 @@ func compileExpr(expr ast.Expr, ctx *context) (matcher.Matcher, bool) {
 			switch expr.Op {
 			case token.REM: // %
 				return matcher.List(x, y), true
+			case token.INC: // ++
+				return matcher.Adjoin(x, y), true
 			default:
 				ctx.addErrorf(expr.Pos(), "invalid token %v", expr.Op)
 			}
diff --git a/tpl/matcher/match.go b/tpl/matcher/match.go
index 23ad7b2d..2ada9480 100644
--- a/tpl/matcher/match.go
+++ b/tpl/matcher/match.go
@@ -19,18 +19,38 @@ package matcher
 import (
 	"errors"
 	"fmt"
+	"log"
 
 	"github.com/goplus/gop/tpl/token"
 	"github.com/goplus/gop/tpl/types"
 )
 
 var (
-	errMultiMismatch = errors.New("multiple mismatch")
-
 	// ErrVarAssigned error
 	ErrVarAssigned = errors.New("variable is already assigned")
+
+	errNoWhitespace  = errors.New("no whitespace")
+	errAdjoinEmpty   = errors.New("adjoin empty")
+	errMultiMismatch = errors.New("multiple mismatch")
+)
+
+// -----------------------------------------------------------------------------
+
+type dbgFlags int
+
+const (
+	DbgFlagMatchVar dbgFlags = 1 << iota
+	DbgFlagAll               = DbgFlagMatchVar
+)
+
+var (
+	enableMatchVar bool
 )
 
+func SetDebug(flags dbgFlags) {
+	enableMatchVar = (flags & DbgFlagMatchVar) != 0
+}
+
 // -----------------------------------------------------------------------------
 
 // Error represents a matching error.
@@ -55,23 +75,34 @@ func isDyn(err error) bool {
 	return false
 }
 
+// RecursiveError represents a recursive error.
+type RecursiveError struct {
+	*Var
+}
+
+func (e RecursiveError) Error() string {
+	return "recursive variable " + e.Name
+}
+
 // -----------------------------------------------------------------------------
 
 // Context represents the context of a matching process.
 type Context struct {
 	Fset    *token.FileSet
 	FileEnd token.Pos
+	toks    []*types.Token
 
-	LastErr error
 	Left    int
+	LastErr error
 }
 
 // NewContext creates a new matching context.
-func NewContext(fset *token.FileSet, fileEnd token.Pos, n int) *Context {
+func NewContext(fset *token.FileSet, fileEnd token.Pos, toks []*types.Token) *Context {
 	return &Context{
 		Fset:    fset,
 		FileEnd: fileEnd,
-		Left:    n,
+		toks:    toks,
+		Left:    len(toks),
 	}
 }
 
@@ -92,17 +123,174 @@ func (p *Context) NewErrorf(pos token.Pos, format string, args ...any) error {
 	return &Error{p.Fset, pos, fmt.Sprintf(format, args...), false}
 }
 
+// -----------------------------------------------------------------------------
+
+// MatchToken represents a matching literal.
+type MatchToken struct {
+	Tok token.Token
+	Lit string
+}
+
+func (p *MatchToken) String() string {
+	return p.Lit
+}
+
+func hasConflictToken(me token.Token, next []any) bool {
+	for _, n := range next {
+		switch n := n.(type) {
+		case *MatchToken:
+			if n.Tok == me {
+				return true
+			}
+		case token.Token:
+			if n == me {
+				return true
+			}
+		default:
+			panic("unreachable")
+		}
+	}
+	return false
+}
+
+func hasConflictMatchToken(me *MatchToken, next []any) bool {
+	for _, n := range next {
+		switch n := n.(type) {
+		case *MatchToken:
+			if n.Tok == me.Tok && n.Lit == me.Lit {
+				return true
+			}
+		case token.Token:
+		default:
+			panic("unreachable")
+		}
+	}
+	return false
+}
+
+func hasConflictMe(me any, next []any) bool {
+	switch me := me.(type) {
+	case token.Token:
+		return hasConflictToken(me, next)
+	case *MatchToken:
+		return hasConflictMatchToken(me, next)
+	}
+	panic("unreachable")
+}
+
+func hasConflict(me []any, next []any) bool {
+	for _, m := range me {
+		if hasConflictMe(m, next) {
+			return true
+		}
+	}
+	return false
+}
+
+func conflictWith(me []any, next [][]any, from int) int {
+	for i, n := from, len(next); i < n; i++ {
+		if hasConflict(me, next[i]) {
+			return i
+		}
+	}
+	return -1
+}
+
 // -----------------------------------------------------------------------------
 // Matcher
 
 // Matcher represents a matcher.
 type Matcher interface {
 	Match(src []*types.Token, ctx *Context) (n int, result any, err error)
+	First(in []any) (first []any, mayEmpty bool) // can be token.Token or *MatchToken
 	IsList() bool
 }
 
 // -----------------------------------------------------------------------------
 
+type gTrue struct{}
+
+func (p gTrue) Match(src []*types.Token, ctx *Context) (n int, result any, err error) {
+	return 0, nil, nil
+}
+
+func (p gTrue) First(in []any) (first []any, mayEmpty bool) {
+	return in, true
+}
+
+func (p gTrue) IsList() bool {
+	return false
+}
+
+// True returns a matcher that always succeeds.
+func True() Matcher {
+	return gTrue{}
+}
+
+// -----------------------------------------------------------------------------
+
+type gWS struct{}
+
+func (p gWS) Match(src []*types.Token, ctx *Context) (n int, result any, err error) {
+	if toks := ctx.toks; len(toks) > len(src) {
+		last := len(toks) - len(src) - 1
+		if toks[last].End() != src[0].Pos {
+			return 0, nil, nil
+		}
+	}
+	return 0, nil, errNoWhitespace
+}
+
+func (p gWS) First(in []any) (first []any, mayEmpty bool) {
+	return in, true
+}
+
+func (p gWS) IsList() bool {
+	return false
+}
+
+// WhiteSpace returns a matcher that matches whitespace.
+func WhiteSpace() Matcher {
+	return gWS{}
+}
+
+// -----------------------------------------------------------------------------
+
+type gString byte
+
+func (p gString) Match(src []*types.Token, ctx *Context) (n int, result any, err error) {
+	if len(src) == 0 {
+		return 0, nil, ctx.NewErrorf(ctx.FileEnd, "expect `%s`, but got EOF", stringType(p))
+	}
+	t := src[0]
+	if t.Tok != token.STRING || t.Lit[0] != byte(p) {
+		return 0, nil, ctx.NewErrorf(t.Pos, "expect `%s`, but got `%v`", stringType(p), t)
+	}
+	return 1, t, nil
+}
+
+func (p gString) First(in []any) (first []any, mayEmpty bool) {
+	return append(in, token.STRING), false
+}
+
+func (p gString) IsList() bool {
+	return false
+}
+
+// String returns a matcher that matches a string literal.
+func String(quoteCh byte) Matcher {
+	return gString(quoteCh)
+}
+
+func stringType(quoteCh gString) string {
+	if quoteCh == '"' {
+		return "QSTRING"
+	}
+	return "RAWSTRING"
+}
+
+// -----------------------------------------------------------------------------
+
 type gToken struct {
 	tok token.Token
 }
@@ -118,6 +306,10 @@ func (p *gToken) Match(src []*types.Token, ctx *Context) (n int, result any, err
 	return 1, t, nil
 }
 
+func (p *gToken) First(in []any) (first []any, mayEmpty bool) {
+	return append(in, p.tok), false
+}
+
 func (p *gToken) IsList() bool {
 	return false
 }
@@ -129,22 +321,23 @@ func Token(tok token.Token) Matcher {
 
 // -----------------------------------------------------------------------------
 
-type gLiteral struct {
-	tok token.Token
-	lit string
-}
+type gLiteral MatchToken
 
 func (p *gLiteral) Match(src []*types.Token, ctx *Context) (n int, result any, err error) {
 	if len(src) == 0 {
-		return 0, nil, ctx.NewErrorf(ctx.FileEnd, "expect `%s`, but got EOF", p.lit)
+		return 0, nil, ctx.NewErrorf(ctx.FileEnd, "expect `%s`, but got EOF", p.Lit)
 	}
 	t := src[0]
-	if t.Tok != p.tok || t.Lit != p.lit {
-		return 0, nil, ctx.NewErrorf(t.Pos, "expect `%s`, but got `%s`", p.lit, t.Lit)
+	if t.Tok != p.Tok || t.Lit != p.Lit {
+		return 0, nil, ctx.NewErrorf(t.Pos, "expect `%s`, but got `%v`", p.Lit, t)
 	}
 	return 1, t, nil
 }
 
+func (p *gLiteral) First(in []any) (first []any, mayEmpty bool) {
+	return append(in, (*MatchToken)(p)), false
+}
+
 func (p *gLiteral) IsList() bool {
 	return false
 }
@@ -156,17 +349,39 @@ func Literal(tok token.Token, lit string) Matcher {
 
 // -----------------------------------------------------------------------------
 
-type gChoice struct {
+// Choices represents a choice matcher.
+type Choices struct {
 	options []Matcher
+	stops   []bool
+}
+
+func (p *Choices) CheckConflicts(conflict func(firsts [][]any, i, at int)) {
+	options := p.options
+	n := len(options)
+	firsts := make([][]any, n)
+	for i, g := range options {
+		firsts[i], _ = g.First(nil)
+	}
+	stops := make([]bool, n)
+	for i, me := range firsts {
+		at := conflictWith(me, firsts, i+1)
+		if at >= 0 {
+			conflict(firsts, i, at)
+		} else {
+			stops[i] = true
+		}
+	}
+	p.stops = stops
 }
 
-func (p *gChoice) Match(src []*types.Token, ctx *Context) (n int, result any, err error) {
+func (p *Choices) Match(src []*types.Token, ctx *Context) (n int, result any, err error) {
 	var nMax = -1
 	var errMax error
 	var multiErr = true
 
-	for _, g := range p.options {
-		if n, result, err = g.Match(src, ctx); err == nil {
+	stops := p.stops // be set by CheckConflicts
+	for i, g := range p.options {
+		if n, result, err = g.Match(src, ctx); err == nil || (n > 0 && stops[i]) {
 			return
 		}
 		if n >= nMax {
@@ -183,13 +398,25 @@ func (p *gChoice) Match(src []*types.Token, ctx *Context) (n int, result any, er
 	return nMax, nil, errMax
 }
 
-func (p *gChoice) IsList() bool {
+func (p *Choices) First(in []any) (first []any, mayEmpty bool) {
+	for _, g := range p.options {
+		var me bool
+		if in, me = g.First(in); me {
+			mayEmpty = true
+		}
+	}
+	first = in
+	return
+}
+
+func (p *Choices) IsList() bool {
 	return false
 }
 
 // Choice: R1 | R2 | ... | Rn
-func Choice(options ...Matcher) Matcher {
-	return &gChoice{options}
+// Should be used with CheckConflicts.
+func Choice(options ...Matcher) *Choices {
+	return &Choices{options, nil}
 }
 
 // -----------------------------------------------------------------------------
@@ -217,11 +444,22 @@ func (p *gSequence) Match(src []*types.Token, ctx *Context) (n int, result any,
 	return
 }
 
+func (p *gSequence) First(in []any) (first []any, mayEmpty bool) {
+	for _, g := range p.items {
+		if in, mayEmpty = g.First(in); !mayEmpty {
+			break
+		}
+	}
+	first = in
+	return
+}
+
 func (p *gSequence) IsList() bool {
 	return true
 }
 
 // Sequence: R1 R2 ... Rn
+// Should length of items > 0
 func Sequence(items ...Matcher) Matcher {
 	return &gSequence{items}
 }
@@ -252,6 +490,12 @@ func (p *gRepeat0) Match(src []*types.Token, ctx *Context) (n int, result any, e
 	}
 }
 
+func (p *gRepeat0) First(in []any) (first []any, mayEmpty bool) {
+	first, _ = p.r.First(in)
+	mayEmpty = true
+	return
+}
+
 func (p *gRepeat0) IsList() bool {
 	return true
 }
@@ -292,6 +536,10 @@ func (p *gRepeat1) Match(src []*types.Token, ctx *Context) (n int, result any, e
 	}
 }
 
+func (p *gRepeat1) First(in []any) (first []any, mayEmpty bool) {
+	return p.r.First(in)
+}
+
 func (p *gRepeat1) IsList() bool {
 	return true
 }
@@ -315,6 +563,12 @@ func (p *gRepeat01) Match(src []*types.Token, ctx *Context) (n int, result any,
 	return
 }
 
+func (p *gRepeat01) First(in []any) (first []any, mayEmpty bool) {
+	first, _ = p.r.First(in)
+	mayEmpty = true
+	return
+}
+
 func (p *gRepeat01) IsList() bool {
 	return false
 }
@@ -333,6 +587,52 @@ func List(a, b Matcher) Matcher {
 
 // -----------------------------------------------------------------------------
 
+type gAdjoin struct {
+	a, b Matcher
+}
+
+func (p *gAdjoin) Match(src []*types.Token, ctx *Context) (n int, result any, err error) {
+	n, ret0, err := p.a.Match(src, ctx)
+	if err != nil {
+		return
+	}
+	if n == 0 {
+		err = errAdjoinEmpty
+		return
+	}
+	n1, ret1, err := p.b.Match(src[n:], ctx)
+	if err != nil && !isDyn(err) {
+		return
+	}
+	if n1 == 0 {
+		err = errAdjoinEmpty
+		return
+	}
+	if src[n-1].End() != src[n].Pos {
+		err = ctx.NewError(src[n].Pos, "not adjoin")
+		return
+	}
+	n += n1
+	result = []any{ret0, ret1}
+	return
+}
+
+func (p *gAdjoin) First(in []any) (first []any, mayEmpty bool) {
+	first, _ = p.a.First(in)
+	return
+}
+
+func (p *gAdjoin) IsList() bool {
+	return true
+}
+
+// Adjoin: R1 ++ R2
+func Adjoin(a, b Matcher) Matcher {
+	return &gAdjoin{a, b}
+}
+
+// -----------------------------------------------------------------------------
+
 type RetProc = func(any) any
 type ListRetProc = func([]any) any
 
@@ -349,6 +649,9 @@ func (p *Var) Match(src []*types.Token, ctx *Context) (n int, result any, err er
 	if g == nil {
 		return 0, nil, ctx.NewErrorf(p.Pos, "variable `%s` not assigned", p.Name)
 	}
+	if enableMatchVar && len(src) > 0 {
+		log.Println("==> Match", p.Name, src[0])
+	}
 	n, result, err = g.Match(src, ctx)
 	if err == nil {
 		if retProc := p.RetProc; retProc != nil {
@@ -391,6 +694,18 @@ func (p *Var) Match(src []*types.Token, ctx *Context) (n int, result any, err er
 	return
 }
 
+func (p *Var) First(in []any) (first []any, mayEmpty bool) {
+	elem := p.Elem
+	if elem != nil {
+		p.Elem = nil // to stop recursion
+		first, mayEmpty = elem.First(in)
+		p.Elem = elem
+	} else {
+		panic(RecursiveError{p})
+	}
+	return
+}
+
 func (p *Var) IsList() bool {
 	return false
 }
diff --git a/tpl/parser/parser.go b/tpl/parser/parser.go
index e04beb3c..f99eaf00 100644
--- a/tpl/parser/parser.go
+++ b/tpl/parser/parser.go
@@ -223,7 +223,7 @@ func (p *parser) lambdaExpr() (start, end token.Pos, ok bool) {
 	}
 }
 
-// parseExpr parses an expression: +term % '|'
+// parseExpr: termList % '|'
 func (p *parser) parseExpr() ast.Expr {
 	termList := p.parseTermList()
 	for p.tok != token.OR {
@@ -240,7 +240,7 @@ func (p *parser) parseExpr() ast.Expr {
 	return &ast.Choice{Options: options}
 }
 
-// parseTermList parses a list of terms: +term
+// parseTermList: +term
 func (p *parser) parseTermList() ast.Expr {
 	terms := make([]ast.Expr, 0, 1)
 	for {
@@ -261,9 +261,9 @@ func (p *parser) parseTermList() ast.Expr {
 	}
 }
 
-// parseTerm parses a term: factor % '%'
+// parseTerm: term2 % '%'
 func (p *parser) parseTerm() (ast.Expr, bool) {
-	x, ok := p.parseFactor()
+	x, ok := p.parseTerm2()
 	if !ok {
 		return nil, false
 	}
@@ -271,7 +271,7 @@ func (p *parser) parseTerm() (ast.Expr, bool) {
 	for p.tok == token.REM {
 		opPos := p.pos
 		p.next()
-		y, ok := p.parseFactor()
+		y, ok := p.parseTerm2()
 		if !ok {
 			p.error(p.pos, "expected factor")
 			return x, false
@@ -286,6 +286,31 @@ func (p *parser) parseTerm() (ast.Expr, bool) {
 	return x, true
 }
 
+// parseTerm2: factor % "++"
+func (p *parser) parseTerm2() (ast.Expr, bool) {
+	x, ok := p.parseFactor()
+	if !ok {
+		return nil, false
+	}
+
+	for p.tok == token.INC {
+		opPos := p.pos
+		p.next()
+		y, ok := p.parseFactor()
+		if !ok {
+			p.error(p.pos, "expected factor")
+			return x, false
+		}
+		x = &ast.BinaryExpr{
+			X:     x,
+			OpPos: opPos,
+			Op:    token.INC,
+			Y:     y,
+		}
+	}
+	return x, true
+}
+
 // parseFactor: IDENT | CHAR | STRING | ('*' | '+' | '?') factor | '(' expr ')'
 func (p *parser) parseFactor() (ast.Expr, bool) {
 	switch tok := p.tok; tok {
diff --git a/tpl/scanner/scanner.go b/tpl/scanner/scanner.go
index 898782f0..32adb0e2 100644
--- a/tpl/scanner/scanner.go
+++ b/tpl/scanner/scanner.go
@@ -50,6 +50,7 @@ type Scanner struct {
 	offset     int  // character offset
 	rdOffset   int  // reading offset (position after current character)
 	lineOffset int  // current line offset
+	unitVal    string
 	insertSemi bool // insert a semicolon before next newline
 
 	// public state - ok to modify
@@ -92,6 +93,15 @@ func (s *Scanner) next() {
 	}
 }
 
+// peek returns the byte following the most recently read character without
+// advancing the scanner. If the scanner is at EOF, peek returns 0.
+func (s *Scanner) peek() byte {
+	if s.rdOffset < len(s.src) {
+		return s.src[s.rdOffset]
+	}
+	return 0
+}
+
 // A Mode value is a set of flags (or 0).
 // They control scanner behavior.
 type Mode uint
@@ -159,6 +169,10 @@ func (s *Scanner) error(offs int, msg string) {
 	s.ErrorCount++
 }
 
+func (s *Scanner) errorf(offs int, format string, args ...any) {
+	s.error(offs, fmt.Sprintf(format, args...))
+}
+
 var prefix = []byte("//line ")
 
 func (s *Scanner) interpretLineComment(text []byte) {
@@ -293,91 +307,191 @@ func digitVal(ch rune) int {
 	switch {
 	case '0' <= ch && ch <= '9':
 		return int(ch - '0')
-	case 'a' <= ch && ch <= 'f':
-		return int(ch - 'a' + 10)
-	case 'A' <= ch && ch <= 'F':
-		return int(ch - 'A' + 10)
+	case 'a' <= lower(ch) && lower(ch) <= 'f':
+		return int(lower(ch) - 'a' + 10)
 	}
 	return 16 // larger than any legal digit val
 }
 
-func (s *Scanner) scanMantissa(base int) {
-	for digitVal(s.ch) < base {
-		s.next()
+func lower(ch rune) rune     { return ('a' - 'A') | ch } // returns lower-case ch iff ch is ASCII letter
+func isDecimal(ch rune) bool { return '0' <= ch && ch <= '9' }
+func isHex(ch rune) bool     { return '0' <= ch && ch <= '9' || 'a' <= lower(ch) && lower(ch) <= 'f' }
+
+// digits accepts the sequence { digit | '_' }.
+// If base <= 10, digits accepts any decimal digit but records
+// the offset (relative to the source start) of a digit >= base
+// in *invalid, if *invalid < 0.
+// digits returns a bitset describing whether the sequence contained
+// digits (bit 0 is set), or separators '_' (bit 1 is set).
+func (s *Scanner) digits(base int, invalid *int) (digsep int) {
+	if base <= 10 {
+		max := rune('0' + base)
+		for isDecimal(s.ch) || s.ch == '_' {
+			ds := 1
+			if s.ch == '_' {
+				ds = 2
+			} else if s.ch >= max && *invalid < 0 {
+				*invalid = int(s.offset) // record invalid rune offset
+			}
+			digsep |= ds
+			s.next()
+		}
+	} else {
+		for isHex(s.ch) || s.ch == '_' {
+			ds := 1
+			if s.ch == '_' {
+				ds = 2
+			}
+			digsep |= ds
+			s.next()
+		}
 	}
+	return
 }
 
-func (s *Scanner) scanNumber(seenDecimalPoint bool) (token.Token, string) {
-	// digitVal(s.ch) < 10
+func (s *Scanner) scanNumber() (token.Token, string) {
 	offs := s.offset
-	tok := token.INT
+	tok := token.ILLEGAL
 
-	if seenDecimalPoint {
-		offs--
-		tok = token.FLOAT
-		s.scanMantissa(10)
-		goto exponent
-	}
+	base := 10        // number base
+	prefix := rune(0) // one of 0 (decimal), '0' (0-octal), 'x', 'o', or 'b'
+	digsep := 0       // bit 0: digit present, bit 1: '_' present
+	invalid := -1     // index of invalid digit in literal, or < 0
 
-	if s.ch == '0' {
-		// int or float
-		offs := s.offset
-		s.next()
-		if s.ch == 'x' || s.ch == 'X' {
-			// hexadecimal int
+	// integer part
+	if s.ch != '.' {
+		tok = token.INT
+		if s.ch == '0' {
 			s.next()
-			s.scanMantissa(16)
-			if s.offset-offs <= 2 {
-				// only scanned "0x" or "0X"
-				s.error(offs, "illegal hexadecimal number")
-			}
-		} else {
-			// octal int or float
-			seenDecimalDigit := false
-			s.scanMantissa(8)
-			if s.ch == '8' || s.ch == '9' {
-				// illegal octal int or float
-				seenDecimalDigit = true
-				s.scanMantissa(10)
-			}
-			if s.ch == '.' || s.ch == 'e' || s.ch == 'E' || s.ch == 'i' {
-				goto fraction
-			}
-			// octal int
-			if seenDecimalDigit {
-				s.error(offs, "illegal octal number")
+			switch lower(s.ch) {
+			case 'x':
+				s.next()
+				base, prefix = 16, 'x'
+			case 'o':
+				s.next()
+				base, prefix = 8, 'o'
+			case 'b':
+				s.next()
+				base, prefix = 2, 'b'
+			default:
+				base, prefix = 8, '0'
+				digsep = 1 // leading 0
 			}
 		}
-		goto exit
+		digsep |= s.digits(base, &invalid)
 	}
 
-	// decimal int or float
-	s.scanMantissa(10)
-
-fraction:
+	// fractional part
 	if s.ch == '.' {
 		tok = token.FLOAT
+		if prefix == 'o' || prefix == 'b' {
+			s.error(s.offset, "invalid radix point in "+litname(prefix))
+		}
 		s.next()
-		s.scanMantissa(10)
+		digsep |= s.digits(base, &invalid)
 	}
 
-exponent:
-	if s.ch == 'e' || s.ch == 'E' {
-		tok = token.FLOAT
+	if digsep&1 == 0 {
+		s.error(s.offset, litname(prefix)+" has no digits")
+	}
+
+	// exponent
+	if e := lower(s.ch); e == 'e' || e == 'p' {
+		switch {
+		case e == 'e' && prefix != 0 && prefix != '0':
+			s.errorf(s.offset, "%q exponent requires decimal mantissa", s.ch)
+		case e == 'p' && prefix != 'x':
+			s.errorf(s.offset, "%q exponent requires hexadecimal mantissa", s.ch)
+		}
 		s.next()
-		if s.ch == '-' || s.ch == '+' {
+		tok = token.FLOAT
+		if s.ch == '+' || s.ch == '-' {
 			s.next()
 		}
-		s.scanMantissa(10)
+		ds := s.digits(10, nil)
+		digsep |= ds
+		if ds&1 == 0 {
+			s.error(s.offset, "exponent has no digits")
+		}
+	} else if prefix == 'x' && tok == token.FLOAT {
+		s.error(s.offset, "hexadecimal mantissa requires a 'p' exponent")
 	}
 
-	if s.ch == 'i' {
-		tok = token.IMAG
-		s.next()
+	if isLetter(s.ch) {
+		id := s.scanIdentifier()
+		switch id {
+		case "i":
+			tok = token.IMAG
+		case "r":
+			tok = token.RAT
+		default:
+			s.unitVal = id
+		}
 	}
 
-exit:
-	return tok, string(s.src[offs:s.offset])
+	lit := string(s.src[offs : s.offset-len(s.unitVal)])
+	if tok == token.INT && invalid >= 0 {
+		s.errorf(invalid, "invalid digit %q in %s", lit[invalid-offs], litname(prefix))
+	}
+	if digsep&2 != 0 {
+		if i := invalidSep(lit); i >= 0 {
+			s.error(offs+i, "'_' must separate successive digits")
+		}
+	}
+
+	return tok, lit
+}
+
+func litname(prefix rune) string {
+	switch prefix {
+	case 'x':
+		return "hexadecimal literal"
+	case 'o', '0':
+		return "octal literal"
+	case 'b':
+		return "binary literal"
+	}
+	return "decimal literal"
+}
+
+// invalidSep returns the index of the first invalid separator in x, or -1.
+func invalidSep(x string) int {
+	x1 := ' ' // prefix char, we only care if it's 'x'
+	d := '.'  // digit, one of '_', '0' (a digit), or '.' (anything else)
+	i := 0
+
+	// a prefix counts as a digit
+	if len(x) >= 2 && x[0] == '0' {
+		x1 = lower(rune(x[1]))
+		if x1 == 'x' || x1 == 'o' || x1 == 'b' {
+			d = '0'
+			i = 2
+		}
+	}
+
+	// mantissa and exponent
+	for ; i < len(x); i++ {
+		p := d // previous digit
+		d = rune(x[i])
+		switch {
+		case d == '_':
+			if p != '0' {
+				return i
+			}
+		case isDecimal(d) || x1 == 'x' && isHex(d):
+			d = '0'
+		default:
+			if p == '_' {
+				return i - 1
+			}
+			d = '.'
+		}
+	}
+	if d == '_' {
+		return len(x) - 1
+	}
+
+	return -1
 }
 
 // scanEscape parses an escape sequence where rune is the accepted
@@ -634,14 +748,21 @@ scanAgain:
 
 	// determine token value
 	insertSemi := false
+	if s.unitVal != "" { // number with unit
+		insertSemi = true
+		t.Pos -= token.Pos(len(s.unitVal))
+		t.Tok, t.Lit = token.UNIT, s.unitVal
+		s.unitVal = ""
+		goto done
+	}
 	switch ch := s.ch; {
 	case isLetter(ch):
+		insertSemi = true
 		t.Lit = s.scanIdentifier()
 		t.Tok = token.IDENT
+	case isDecimal(ch) || ch == '.' && isDecimal(rune(s.peek())):
 		insertSemi = true
-	case '0' <= ch && ch <= '9':
-		insertSemi = true
-		t.Tok, t.Lit = s.scanNumber(false)
+		t.Tok, t.Lit = s.scanNumber()
 	default:
 		s.next() // always make progress
 		switch ch {
@@ -674,15 +795,11 @@ scanAgain:
 		case ':':
 			t.Tok = s.switch2(token.COLON, token.DEFINE)
 		case '.':
-			if '0' <= s.ch && s.ch <= '9' {
-				insertSemi = true
-				t.Tok, t.Lit = s.scanNumber(true)
-			} else if s.ch == '.' {
+			// fractions starting with a '.' are handled by outer switch
+			if s.ch == '.' && s.peek() == '.' {
 				s.next()
-				if s.ch == '.' {
-					s.next()
-					t.Tok = token.ELLIPSIS
-				}
+				s.next() // consume last '.'
+				t.Tok = token.ELLIPSIS
 			} else {
 				t.Tok = token.PERIOD
 			}
@@ -721,8 +838,8 @@ scanAgain:
 					insertSemi = true
 				}
 			}
-		case '*':
-			t.Tok = s.switch2(token.MUL, token.MUL_ASSIGN)
+		case '*': // ** *=
+			t.Tok = s.switch3(token.MUL, token.MUL_ASSIGN, '*', token.POW)
 		case '/':
 			if s.ch == '/' || s.ch == '*' {
 				// comment
@@ -781,6 +898,9 @@ scanAgain:
 			t.Tok = s.switch3(token.ASSIGN, token.EQ, '>', token.DRARROW)
 		case '!':
 			t.Tok = s.switch2(token.NOT, token.NE)
+			if t.Tok == token.NOT {
+				insertSemi = true
+			}
 		case '&':
 			if s.ch == '^' {
 				s.next()
@@ -792,6 +912,7 @@ scanAgain:
 			t.Tok = s.switch3(token.OR, token.OR_ASSIGN, '|', token.LOR)
 		case '?':
 			t.Tok = token.QUESTION
+			insertSemi = true
 		case '$':
 			t.Tok = token.ENV
 		case '~':
@@ -808,9 +929,10 @@ scanAgain:
 			t.Lit = string(ch)
 		}
 	}
+
+done:
 	if s.mode&NoInsertSemis == 0 {
 		s.insertSemi = insertSemi
 	}
-
 	return
 }
diff --git a/tpl/token/token.go b/tpl/token/token.go
index 6dd8e6ac..a3101773 100644
--- a/tpl/token/token.go
+++ b/tpl/token/token.go
@@ -61,6 +61,8 @@ const (
 	IMAG   // 123.45i
 	CHAR   // 'a'
 	STRING // "abc"
+	RAT    // 3r, 3.4r
+	UNIT   // 1m, 2.3s, 3ms, 4us, 5ns, 6.5m, 7h, 8d, 9w, 10y
 	literal_end
 
 	ADD = '+'
@@ -131,6 +133,7 @@ const (
 	DRARROW   // =>
 	SRARROW   // ->
 	BIDIARROW // <>
+	POW       // **
 
 	operator_end
 )
@@ -149,6 +152,8 @@ var tokens = [...]string{
 	IMAG:   "IMAG",
 	CHAR:   "CHAR",
 	STRING: "STRING",
+	RAT:    "RAT",
+	UNIT:   "UNIT",
 
 	ADD: "+",
 	SUB: "-",
@@ -214,6 +219,7 @@ var tokens = [...]string{
 	DRARROW:   "=>",
 	SRARROW:   "->",
 	BIDIARROW: "<>",
+	POW:       "**",
 }
 
 const (
diff --git a/tpl/tpl.go b/tpl/tpl.go
index 366b51d9..a9dc15eb 100644
--- a/tpl/tpl.go
+++ b/tpl/tpl.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"reflect"
 
 	"github.com/goplus/gop/parser/iox"
 	"github.com/goplus/gop/tpl/ast"
@@ -29,10 +30,58 @@ import (
 	"github.com/goplus/gop/tpl/scanner"
 	"github.com/goplus/gop/tpl/token"
 	"github.com/goplus/gop/tpl/types"
+	"github.com/qiniu/x/errors"
 )
 
 // -----------------------------------------------------------------------------
 
+var showConflict = true
+
+// ShowConflict sets the flag to show or hide conflicts.
+func ShowConflict(f bool) int {
+	showConflict = f
+	return 0
+}
+
+func onConflictHidden(fset *token.FileSet, c *ast.Choice, firsts [][]any, i, at int) {
+}
+
+// -----------------------------------------------------------------------------
+
+func relocatePos(ePos *token.Position, filename string, line, col int) {
+	ePos.Filename = filename
+	if ePos.Line == line {
+		ePos.Column += col - 1
+	} else {
+		ePos.Line += line - 1
+	}
+}
+
+// Relocate relocates the error positions.
+func Relocate(err error, filename string, line, col int) error {
+	switch e := err.(type) {
+	case *matcher.Error:
+		pos := e.Fset.Position(e.Pos)
+		relocatePos(&pos, filename, line, col)
+		return &scanner.Error{Pos: pos, Msg: e.Msg}
+	case errors.List:
+		for i, ie := range e {
+			e[i] = Relocate(ie, filename, line, col)
+		}
+	case scanner.ErrorList:
+		for _, ie := range e {
+			relocatePos(&ie.Pos, filename, line, col)
+		}
+	case *scanner.Error:
+		relocatePos(&e.Pos, filename, line, col)
+	default:
+		panic("todo: " + reflect.TypeOf(err).String())
+	}
+	return err
+}
+
+// -----------------------------------------------------------------------------
+
 // Compiler represents a TPL compiler.
 type Compiler struct {
 	cl.Result
@@ -41,13 +90,42 @@ type Compiler struct {
 // New creates a new TPL compiler.
 // params: ruleName1, retProc1, ..., ruleNameN, retProcN
 func New(src any, params ...any) (ret Compiler, err error) {
-	return FromFile(nil, "", src, params...)
+	conf := &cl.Config{
+		RetProcs: retProcs(params),
+	}
+	if !showConflict {
+		conf.OnConflict = onConflictHidden
+	}
+	return FromFile(nil, "", src, conf)
+}
+
+// NewEx creates a new TPL compiler.
+// params: ruleName1, retProc1, ..., ruleNameN, retProcN
+func NewEx(src any, filename string, line, col int, params ...any) (ret Compiler, err error) {
+	conf := &cl.Config{
+		RetProcs: retProcs(params),
+	}
+	if showConflict {
+		conf.OnConflict = func(fset *token.FileSet, c *ast.Choice, firsts [][]any, i, at int) {
+			if showConflict {
+				pos := fset.Position(c.Options[i].Pos())
+				relocatePos(&pos, filename, line, col)
+				cl.LogConflict(pos, firsts, i, at)
+			}
+		}
+	} else {
+		conf.OnConflict = onConflictHidden
+	}
+	ret, err = FromFile(nil, "", src, conf)
+	if err != nil {
+		err = Relocate(err, filename, line, col)
+	}
+	return
 }
 
 // FromFile creates a new TPL compiler from a file.
 // fset can be nil.
-// params: ruleName1, retProc1, ..., ruleNameN, retProcN
-func FromFile(fset *token.FileSet, filename string, src any, params ...any) (ret Compiler, err error) {
+func FromFile(fset *token.FileSet, filename string, src any, conf *cl.Config) (ret Compiler, err error) {
 	if fset == nil {
 		fset = token.NewFileSet()
 	}
@@ -55,7 +133,7 @@ func FromFile(fset *token.FileSet, filename string, src any, params ...any) (ret
 	if err != nil {
 		return
 	}
-	ret.Result, err = cl.NewEx(retProcs(params), fset, f)
+	ret.Result, err = cl.NewEx(conf, fset, f)
 	return
 }
 
@@ -76,6 +154,9 @@ func retProcs(params []any) map[string]any {
 
 // -----------------------------------------------------------------------------
 
+// Error represents a matching error.
+type Error = matcher.Error
+
 // A Token is a lexical unit returned by Scan.
 type Token = types.Token
 
@@ -138,7 +219,7 @@ func (p *MatchState) Next() *Token {
 	if n > 0 {
 		return p.Toks[len(p.Toks)-n]
 	}
-	return &Token{Tok: token.EOF}
+	return &Token{Tok: token.EOF, Pos: p.Ctx.FileEnd}
 }
 
 // Match matches a source file.
@@ -172,7 +253,7 @@ func (p *Compiler) Match(filename string, src any, conf *Config) (ms MatchState,
 		}
 		toks = append(toks, &t)
 	}
-	ms.Ctx = matcher.NewContext(fset, token.Pos(f.Base()+len(b)), len(toks))
+	ms.Ctx = matcher.NewContext(fset, token.Pos(f.Base()+len(b)), toks)
 	ms.N, result, err = p.Doc.Match(toks, ms.Ctx)
 	ms.Ctx.SetLastError(len(toks)-ms.N, err)
 	if err != nil {
@@ -269,8 +350,10 @@ retry:
 			}
 			fmt.Print(prefix, "]\n")
 		}
+	case nil:
+		fmt.Fprint(w, prefix, "nil\n")
 	default:
-		panic("unexpected node")
+		panic("unexpected node: " + reflect.TypeOf(ret).String())
 	}
 }
 
diff --git a/tpl/variant/delay/delay.go b/tpl/variant/delay/delay.go
index b857288f..eb5a4436 100644
--- a/tpl/variant/delay/delay.go
+++ b/tpl/variant/delay/delay.go
@@ -100,12 +100,19 @@ func Call(needList bool, name string, arglist any) any {
 	}
 }
 
+// CallObject delays to call a function object.
+func CallObject(needList bool, fn any, arglist any) any {
+	return func() any {
+		return variant.CallObject(needList, fn, arglist)
+	}
+}
+
 // -----------------------------------------------------------------------------
 
 // ValueOf delays to get a value.
-func ValueOf(name string, get func(name string) (any, bool)) any {
+func ValueOf(name string, getval func(name string) (any, bool)) any {
 	return func() any {
-		v, ok := get(name)
+		v, ok := getval(name)
 		if !ok {
 			panic(name + " is undefined")
 		}
@@ -114,9 +121,9 @@ func ValueOf(name string, get func(name string) (any, bool)) any {
 }
 
 // SetValue delays to set a value.
-func SetValue(name string, expr any, set func(name string, val any)) any {
+func SetValue(name string, setval func(name string, val any), expr any) any {
 	return func() any {
-		set(name, Eval(expr))
+		setval(name, Eval(expr))
 		return nil
 	}
 }
diff --git a/tpl/variant/module.go b/tpl/variant/module.go
index 13bc8d9c..fc47058f 100644
--- a/tpl/variant/module.go
+++ b/tpl/variant/module.go
@@ -86,6 +86,21 @@ func Call(needList bool, name string, arglist any) any {
 	panic("function not found: " + name)
 }
 
+// CallObject calls a function object.
+func CallObject(needList bool, fn any, arglist any) any {
+	if fn, ok := Eval(fn).(func(...any) any); ok {
+		var args []any
+		if arglist != nil {
+			args = arglist.([]any)
+			if needList {
+				args = tpl.List(args)
+			}
+		}
+		return fn(args...)
+	}
+	panic("call of non function")
+}
+
 // -----------------------------------------------------------------------------
 
 // InitUniverse initializes the universe module with the specified modules.
diff --git a/x/gopprojs/proj.go b/x/gopprojs/proj.go
index e27e4d57..195af794 100644
--- a/x/gopprojs/proj.go
+++ b/x/gopprojs/proj.go
@@ -24,18 +24,22 @@ import (
 
 // -----------------------------------------------------------------------------
 
+// Proj is the interface for a project
 type Proj = interface {
 	projObj()
 }
 
+// FilesProj represents a project with files
 type FilesProj struct {
 	Files []string
 }
 
+// PkgPathProj represents a project with a package path
 type PkgPathProj struct {
 	Path string
 }
 
+// DirProj represents a project with a directory
 type DirProj struct {
 	Dir string
 }
@@ -46,6 +50,8 @@ func (p *DirProj) projObj()     {}
 
 // -----------------------------------------------------------------------------
 
+// ParseOne parses the first argument and returns a Proj object
+// If the first argument is a file, it continues to parse subsequent arguments.
 func ParseOne(args ...string) (proj Proj, next []string, err error) {
 	if len(args) == 0 {
 		return nil, nil, syscall.ENOENT
@@ -83,6 +89,7 @@ func isLocal(ns string) bool {
 
 // -----------------------------------------------------------------------------
 
+// ParseAll parses all arguments and returns a slice of Proj objects
 func ParseAll(args ...string) (projs []Proj, err error) {
 	var hasFiles, hasNotFiles bool
 	for {
@@ -104,6 +111,7 @@ func ParseAll(args ...string) (projs []Proj, err error) {
 }
 
 var (
+	// ErrMixedFilesProj is returned when a project contains both files and non-files
 	ErrMixedFilesProj = errors.New("mixed files project")
 )
 


Test patch:
diff --git a/cl/_testgop/domaintext-tpl/out.go b/cl/_testgop/domaintext-tpl/out.go
index af0289cf..ae63376b 100644
--- a/cl/_testgop/domaintext-tpl/out.go
+++ b/cl/_testgop/domaintext-tpl/out.go
@@ -3,7 +3,7 @@ package main
 import "github.com/goplus/gop/tpl"
 
 func main() {
-	cl, err := tpl.New(`expr = INT % ("+" | "-")`)
+	cl, err := tpl.NewEx(`expr = INT % ("+" | "-")`, "cl/_testgop/domaintext-tpl/in.gop", 1, 15)
 	cl.ParseExpr("1+2", nil)
 	_ = err
 }
diff --git a/cl/_testgop/domaintpl/out.go b/cl/_testgop/domaintpl/out.go
index 094a464a..81ca5fde 100644
--- a/cl/_testgop/domaintpl/out.go
+++ b/cl/_testgop/domaintpl/out.go
@@ -3,7 +3,7 @@ package main
 import "github.com/goplus/gop/tpl"
 
 func main() {
-	tpl.New(`
+	tpl.NewEx(`
 file = stmts => {
 	return self
 }
@@ -51,7 +51,7 @@ parenExpr = "(" expr ")"
 exprlist = expr % ","
 
 namelist = IDENT % ","
-`, "file", func(self interface{}) interface{} {
+`, "cl/_testgop/domaintpl/in.gop", 1, 4, "file", func(self interface{}) interface{} {
 		return self
 	}, "stmts", func(self []interface{}) interface{} {
 		return func() (_gop_ret []interface{}) {
diff --git a/cl/cltest/cltest.go b/cl/cltest/cltest.go
index aff9a1b1..c0970b22 100644
--- a/cl/cltest/cltest.go
+++ b/cl/cltest/cltest.go
@@ -18,6 +18,10 @@ package cltest
 
 import (
 	"bytes"
+	"fmt"
+	"go/ast"
+	"go/build"
+	"go/types"
 	"io/fs"
 	"log"
 	"os"
@@ -25,6 +29,8 @@ import (
 	"strings"
 	"testing"
 
+	goparser "go/parser"
+
 	"github.com/goplus/gogen"
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/parser"
@@ -33,14 +39,16 @@ import (
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/gop/token"
 	"github.com/goplus/gop/tool"
+	"github.com/goplus/mod"
 	"github.com/goplus/mod/env"
 	"github.com/goplus/mod/gopmod"
 	"github.com/goplus/mod/modfile"
 )
 
 var (
-	Gop  *env.Gop
-	Conf *cl.Config
+	GopRoot string
+	Gop     *env.Gop
+	Conf    *cl.Config
 )
 
 func init() {
@@ -49,6 +57,7 @@ func init() {
 	cl.SetDebug(cl.DbgFlagAll | cl.FlagNoMarkAutogen)
 	fset := token.NewFileSet()
 	imp := tool.NewImporter(nil, Gop, fset)
+	GopRoot, _, _ = mod.FindGoMod("")
 	Conf = &cl.Config{
 		Fset:          fset,
 		Importer:      imp,
@@ -201,8 +210,39 @@ func testFrom(t *testing.T, pkgDir, sel string) {
 			confCopy.Importer = tool.NewImporter(mod, Gop, conf.Fset)
 			conf = &confCopy
 		}
+	} else {
+		confCopy := *Conf
+		confCopy.RelativeBase = GopRoot
+		conf = &confCopy
 	}
 	DoFS(t, conf, fsx.Local, pkgDir, filter, "main", expected)
 }
 
+func Go1Point() int {
+	for i := len(build.Default.ReleaseTags) - 1; i >= 0; i-- {
+		var version int
+		if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil {
+			continue
+		}
+		return version
+	}
+	panic("bad release tags")
+}
+
+func EnableTypesalias() bool {
+	ver := Go1Point()
+	if ver < 22 {
+		return false
+	} else if ver > 22 {
+		_, ok := types.Universe.Lookup("any").Type().(*types.Interface)
+		return !ok
+	} else {
+		fset := token.NewFileSet()
+		f, _ := goparser.ParseFile(fset, "a.go", "package p; type A = int", goparser.SkipObjectResolution)
+		pkg, _ := new(types.Config).Check("p", fset, []*ast.File{f}, nil)
+		_, ok := pkg.Scope().Lookup("A").Type().(*types.Basic)
+		return !ok
+	}
+}
+
 // -----------------------------------------------------------------------------
diff --git a/cl/compile_gop_test.go b/cl/compile_gop_test.go
index d6fa13e0..3a799df7 100644
--- a/cl/compile_gop_test.go
+++ b/cl/compile_gop_test.go
@@ -933,14 +933,22 @@ func main() {
 }
 
 func TestOverloadNamed(t *testing.T) {
-	gopClTest(t, `
+	var excepted string
+	if gotypesalias {
+		excepted = `package main
+
 import "github.com/goplus/gop/cl/internal/overload/bar"
 
-var a bar.Var[int]
-var b bar.Var[bar.M]
-c := bar.Var(string)
-d := bar.Var(bar.M)
-`, `package main
+var a bar.Var__0[int]
+var b bar.Var__1[bar.M]
+
+func main() {
+	c := bar.Gopx_Var_Cast__0[string]()
+	d := bar.Gopx_Var_Cast__1[bar.M]()
+}
+`
+	} else {
+		excepted = `package main
 
 import "github.com/goplus/gop/cl/internal/overload/bar"
 
@@ -951,7 +959,16 @@ func main() {
 	c := bar.Gopx_Var_Cast__0[string]()
 	d := bar.Gopx_Var_Cast__1[map[string]any]()
 }
-`)
+`
+	}
+	gopClTest(t, `
+import "github.com/goplus/gop/cl/internal/overload/bar"
+
+var a bar.Var[int]
+var b bar.Var[bar.M]
+c := bar.Var(string)
+d := bar.Var(bar.M)
+`, excepted)
 }
 
 func TestMixedOverloadNamed(t *testing.T) {
diff --git a/cl/compile_test.go b/cl/compile_test.go
index 88521d52..89e0476b 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -31,7 +31,8 @@ const (
 )
 
 var (
-	gblConfLine *cl.Config
+	gblConfLine  *cl.Config
+	gotypesalias bool
 )
 
 func init() {
@@ -45,6 +46,7 @@ func init() {
 		NoFileLine:    false,
 		NoAutoGenMain: true,
 	}
+	gotypesalias = cltest.EnableTypesalias()
 }
 
 func gopClNamedTest(t *testing.T, name string, gopcode, expected string) {
@@ -1955,13 +1957,15 @@ func main() {
 }
 
 func TestStructType(t *testing.T) {
-	gopClTest(t, `
-type bar = foo
+	var expect string
+	if gotypesalias {
+		expect = `package main
 
+type bar = foo
 type foo struct {
 	p *bar
 	A int
-	B string "tag1:123"
+	B string ` + "`tag1:123`" + `
 }
 
 func main() {
@@ -1970,13 +1974,15 @@ func main() {
 	}
 	type b = a
 }
-`, `package main
+`
+	} else {
+		expect = `package main
 
 type bar = foo
 type foo struct {
 	p *foo
 	A int
-	B string `+"`tag1:123`"+`
+	B string ` + "`tag1:123`" + `
 }
 
 func main() {
@@ -1985,7 +1991,24 @@ func main() {
 	}
 	type b = a
 }
-`)
+`
+	}
+	gopClTest(t, `
+type bar = foo
+
+type foo struct {
+	p *bar
+	A int
+	B string "tag1:123"
+}
+
+func main() {
+	type a struct {
+		p *a
+	}
+	type b = a
+}
+`, expect)
 }
 
 func TestDeferGo(t *testing.T) {
diff --git a/cl/typeparams_test.go b/cl/typeparams_test.go
index b1a679e0..c2da0777 100644
--- a/cl/typeparams_test.go
+++ b/cl/typeparams_test.go
@@ -136,6 +136,56 @@ func main() {
 }
 
 func TestTypeParamsType(t *testing.T) {
+	var expect string
+	if gotypesalias {
+		expect = `package main
+
+import "fmt"
+
+type DataString = Data[string]
+type SliceString = Slice[[]string, string]
+
+func main() {
+	fmt.Println(Data[int]{1}.v)
+	fmt.Println(DataString{"hello"}.v)
+	fmt.Println(Data[int]{100}.v)
+	fmt.Println(Data[string]{"hello"}.v)
+	fmt.Println(Data[struct {
+		X int
+		Y int
+	}]{}.v.X)
+	v1 := Slice[[]int, int]{}
+	v2 := SliceString{}
+	v3 := Slice[[]int, int]{}
+	v3.Append([]int{1, 2, 3, 4}...)
+	v3.Append2([]int{1, 2, 3, 4}...)
+}
+`
+	} else {
+		expect = `package main
+
+import "fmt"
+
+type DataString = Data[string]
+type SliceString = Slice[[]string, string]
+
+func main() {
+	fmt.Println(Data[int]{1}.v)
+	fmt.Println(Data[string]{"hello"}.v)
+	fmt.Println(Data[int]{100}.v)
+	fmt.Println(Data[string]{"hello"}.v)
+	fmt.Println(Data[struct {
+		X int
+		Y int
+	}]{}.v.X)
+	v1 := Slice[[]int, int]{}
+	v2 := Slice[[]string, string]{}
+	v3 := Slice[[]int, int]{}
+	v3.Append([]int{1, 2, 3, 4}...)
+	v3.Append2([]int{1, 2, 3, 4}...)
+}
+`
+	}
 	gopMixedClTest(t, "main", `package main
 type Data[T any] struct {
 	v T
@@ -178,29 +228,7 @@ v2 := SliceString{}
 v3 := Slice[[]int,int]{}
 v3.Append([1,2,3,4]...)
 v3.Append2([1,2,3,4]...)
-`, `package main
-
-import "fmt"
-
-type DataString = Data[string]
-type SliceString = Slice[[]string, string]
-
-func main() {
-	fmt.Println(Data[int]{1}.v)
-	fmt.Println(Data[string]{"hello"}.v)
-	fmt.Println(Data[int]{100}.v)
-	fmt.Println(Data[string]{"hello"}.v)
-	fmt.Println(Data[struct {
-		X int
-		Y int
-	}]{}.v.X)
-	v1 := Slice[[]int, int]{}
-	v2 := Slice[[]string, string]{}
-	v3 := Slice[[]int, int]{}
-	v3.Append([]int{1, 2, 3, 4}...)
-	v3.Append2([]int{1, 2, 3, 4}...)
-}
-`)
+`, expect)
 }
 
 func TestTypeParamsComparable(t *testing.T) {
@@ -314,7 +342,13 @@ _ = At[int]
 }
 
 func TestTypeParamsErrInferFunc(t *testing.T) {
-	mixedErrorTest(t, `b.gop:2:5: cannot infer T2 (/foo/a.go:4:21)`, `
+	var expect string
+	if cltest.Go1Point() == 24 {
+		expect = `b.gop:2:5: cannot infer T2 (declared at /foo/a.go:4:21)`
+	} else {
+		expect = `b.gop:2:5: cannot infer T2 (/foo/a.go:4:21)`
+	}
+	mixedErrorTest(t, expect, `
 package main
 
 func Loader[T1 any, T2 any](p1 T1, p2 T2) T1 {
diff --git a/tpl/parser/_testdata/adjoin/in.gop b/tpl/parser/_testdata/adjoin/in.gop
new file mode 100644
index 00000000..50f232d0
--- /dev/null
+++ b/tpl/parser/_testdata/adjoin/in.gop
@@ -0,0 +1,1 @@
+doc = IDENT ++ RAWSTRING | IDENT SPACE "{" "}"
diff --git a/tpl/parser/_testdata/adjoin/out.expect b/tpl/parser/_testdata/adjoin/out.expect
new file mode 100644
index 00000000..d73ff61a
--- /dev/null
+++ b/tpl/parser/_testdata/adjoin/out.expect
@@ -0,0 +1,27 @@
+ast.Rule:
+  Name:
+    ast.Ident:
+      Name: doc
+  Expr:
+    ast.Choice:
+      Options:
+        ast.BinaryExpr:
+          X:
+            ast.Ident:
+              Name: IDENT
+          Op: ++
+          Y:
+            ast.Ident:
+              Name: RAWSTRING
+        ast.Sequence:
+          Items:
+            ast.Ident:
+              Name: IDENT
+            ast.Ident:
+              Name: SPACE
+            ast.BasicLit:
+              Kind: STRING
+              Value: "{"
+            ast.BasicLit:
+              Kind: STRING
+              Value: "}"
diff --git a/tpl/parser/parser_test.go b/tpl/parser/parser_test.go
index d49f2bf3..c296d3cc 100644
--- a/tpl/parser/parser_test.go
+++ b/tpl/parser/parser_test.go
@@ -52,11 +52,8 @@ func testFrom(t *testing.T, pkgDir, sel string) {
 		}
 		t.Fatal("ParseFile failed:", err, reflect.TypeOf(err))
 	}
-	b, err := os.ReadFile(pkgDir + "/out.expect")
-	if err != nil {
-		t.Fatal("Parsing", pkgDir, "-", err)
-	}
-	parsertest.Expect(t, f, string(b))
+	b, _ := os.ReadFile(pkgDir + "/out.expect")
+	parsertest.Expect(t, pkgDir+"/result.txt", f, b)
 }
 
 func testFromDir(t *testing.T, sel, relDir string) {
diff --git a/tpl/parser/parsertest/parsertest.go b/tpl/parser/parsertest/parsertest.go
index 8d1dd68e..0fee17bc 100644
--- a/tpl/parser/parsertest/parsertest.go
+++ b/tpl/parser/parsertest/parsertest.go
@@ -29,6 +29,35 @@ import (
 	"github.com/goplus/gop/tpl/token"
 )
 
+// -----------------------------------------------------------------------------
+
+func Diff(t *testing.T, outfile string, dst, src []byte) bool {
+	line := 1
+	offs := 0 // line offset
+	for i := 0; i < len(dst) && i < len(src); i++ {
+		d := dst[i]
+		s := src[i]
+		if d != s {
+			os.WriteFile(outfile, dst, 0644)
+			t.Errorf("dst:%d: %s\n", line, dst[offs:])
+			t.Errorf("src:%d: %s\n", line, src[offs:])
+			return true
+		}
+		if s == '\n' {
+			line++
+			offs = i + 1
+		}
+	}
+	if len(dst) != len(src) {
+		os.WriteFile(outfile, dst, 0644)
+		t.Errorf("len(dst) = %d, len(src) = %d\ndst = %q\nsrc = %q", len(dst), len(src), dst, src)
+		return true
+	}
+	return false
+}
+
+// -----------------------------------------------------------------------------
+
 var (
 	tyNode   = reflect.TypeOf((*ast.Node)(nil)).Elem()
 	tyString = reflect.TypeOf("")
@@ -89,12 +118,12 @@ func Fprint(w io.Writer, f *ast.File) {
 }
 
 // Expect asserts a tpl AST equals output or not.
-func Expect(t *testing.T, f *ast.File, expected string) {
+func Expect(t *testing.T, outfile string, f *ast.File, expected []byte) {
 	b := bytes.NewBuffer(nil)
 	Fprint(b, f)
-	output := b.String()
-	if expected != output {
-		fmt.Fprint(os.Stderr, output)
+	if Diff(t, outfile, b.Bytes(), []byte(expected)) {
 		t.Fatal("tpl.Parser: unexpect result")
 	}
 }
+
+// -----------------------------------------------------------------------------
diff --git a/tpl/scanner/_testdata/cstr/go.expect b/tpl/scanner/_testdata/cstr/go.expect
new file mode 100644
index 00000000..aee852ee
--- /dev/null
+++ b/tpl/scanner/_testdata/cstr/go.expect
@@ -0,0 +1,14 @@
+1 IDENT c
+2 . 
+3 IDENT printf
+10 IDENT c
+11 STRING "Hello"
+18 ; 
+
+19 IDENT std
+22 . 
+23 IDENT print
+29 IDENT py
+31 STRING "Hello"
+38 ; 
+
diff --git a/tpl/scanner/_testdata/cstr/gop.expect b/tpl/scanner/_testdata/cstr/gop.expect
new file mode 100644
index 00000000..16ea4142
--- /dev/null
+++ b/tpl/scanner/_testdata/cstr/gop.expect
@@ -0,0 +1,12 @@
+1 IDENT c
+2 . 
+3 IDENT printf
+10 CSTRING "Hello"
+18 ; 
+
+19 IDENT std
+22 . 
+23 IDENT print
+29 PYSTRING "Hello"
+38 ; 
+
diff --git a/tpl/scanner/_testdata/cstr/in.gop b/tpl/scanner/_testdata/cstr/in.gop
new file mode 100644
index 00000000..ba2ad819
--- /dev/null
+++ b/tpl/scanner/_testdata/cstr/in.gop
@@ -0,0 +1,2 @@
+c.printf c"Hello"
+std.print py"Hello"
diff --git a/tpl/scanner/_testdata/cstr/tpl.expect b/tpl/scanner/_testdata/cstr/tpl.expect
new file mode 100644
index 00000000..aee852ee
--- /dev/null
+++ b/tpl/scanner/_testdata/cstr/tpl.expect
@@ -0,0 +1,14 @@
+1 IDENT c
+2 . 
+3 IDENT printf
+10 IDENT c
+11 STRING "Hello"
+18 ; 
+
+19 IDENT std
+22 . 
+23 IDENT print
+29 IDENT py
+31 STRING "Hello"
+38 ; 
+
diff --git a/tpl/scanner/_testdata/num/go.expect b/tpl/scanner/_testdata/num/go.expect
new file mode 100644
index 00000000..6877f962
--- /dev/null
+++ b/tpl/scanner/_testdata/num/go.expect
@@ -0,0 +1,15 @@
+1 IDENT echo
+6 INT 1_002
+11 ; 
+
+12 IDENT echo
+17 INT 2
+18 + 
+19 IMAG 3i
+21 ; 
+
+22 IDENT echo
+27 FLOAT 2.
+29 IDENT string
+35 ; 
+
diff --git a/tpl/scanner/_testdata/num/gop.expect b/tpl/scanner/_testdata/num/gop.expect
new file mode 100644
index 00000000..e97b7983
--- /dev/null
+++ b/tpl/scanner/_testdata/num/gop.expect
@@ -0,0 +1,15 @@
+1 IDENT echo
+6 INT 1_002
+11 ; 
+
+12 IDENT echo
+17 INT 2
+18 + 
+19 IMAG 3i
+21 ; 
+
+22 IDENT echo
+27 FLOAT 2.
+29 UNIT string
+35 ; 
+
diff --git a/tpl/scanner/_testdata/num/in.gop b/tpl/scanner/_testdata/num/in.gop
new file mode 100644
index 00000000..73da6cfa
--- /dev/null
+++ b/tpl/scanner/_testdata/num/in.gop
@@ -0,0 +1,3 @@
+echo 1_002
+echo 2+3i
+echo 2.string
diff --git a/tpl/scanner/_testdata/num/tpl.expect b/tpl/scanner/_testdata/num/tpl.expect
new file mode 100644
index 00000000..e97b7983
--- /dev/null
+++ b/tpl/scanner/_testdata/num/tpl.expect
@@ -0,0 +1,15 @@
+1 IDENT echo
+6 INT 1_002
+11 ; 
+
+12 IDENT echo
+17 INT 2
+18 + 
+19 IMAG 3i
+21 ; 
+
+22 IDENT echo
+27 FLOAT 2.
+29 UNIT string
+35 ; 
+
diff --git a/tpl/scanner/_testdata/pow/go.expect b/tpl/scanner/_testdata/pow/go.expect
new file mode 100644
index 00000000..ceb36fbd
--- /dev/null
+++ b/tpl/scanner/_testdata/pow/go.expect
@@ -0,0 +1,14 @@
+1 IDENT echo
+6 INT 2
+7 * 
+8 * 
+9 INT 3
+10 ; 
+
+11 IDENT echo
+16 STRING `abc`
+21 [ 
+22 INT 0
+23 ] 
+24 ; 
+
diff --git a/tpl/scanner/_testdata/pow/gop.expect b/tpl/scanner/_testdata/pow/gop.expect
new file mode 100644
index 00000000..ceb36fbd
--- /dev/null
+++ b/tpl/scanner/_testdata/pow/gop.expect
@@ -0,0 +1,14 @@
+1 IDENT echo
+6 INT 2
+7 * 
+8 * 
+9 INT 3
+10 ; 
+
+11 IDENT echo
+16 STRING `abc`
+21 [ 
+22 INT 0
+23 ] 
+24 ; 
+
diff --git a/tpl/scanner/_testdata/pow/in.gop b/tpl/scanner/_testdata/pow/in.gop
new file mode 100644
index 00000000..a80a7327
--- /dev/null
+++ b/tpl/scanner/_testdata/pow/in.gop
@@ -0,0 +1,2 @@
+echo 2**3
+echo `abc`[0]
diff --git a/tpl/scanner/_testdata/pow/tpl.expect b/tpl/scanner/_testdata/pow/tpl.expect
new file mode 100644
index 00000000..467e196e
--- /dev/null
+++ b/tpl/scanner/_testdata/pow/tpl.expect
@@ -0,0 +1,13 @@
+1 IDENT echo
+6 INT 2
+7 ** 
+9 INT 3
+10 ; 
+
+11 IDENT echo
+16 STRING `abc`
+21 [ 
+22 INT 0
+23 ] 
+24 ; 
+
diff --git a/tpl/scanner/_testdata/rat/go.expect b/tpl/scanner/_testdata/rat/go.expect
new file mode 100644
index 00000000..e3c89db5
--- /dev/null
+++ b/tpl/scanner/_testdata/rat/go.expect
@@ -0,0 +1,12 @@
+1 IDENT echo
+6 INT 1
+7 / 
+8 INT 3
+9 IDENT r
+10 ; 
+
+11 IDENT echo
+16 FLOAT 1.23
+20 IDENT r
+21 ; 
+
diff --git a/tpl/scanner/_testdata/rat/gop.expect b/tpl/scanner/_testdata/rat/gop.expect
new file mode 100644
index 00000000..5f75eba6
--- /dev/null
+++ b/tpl/scanner/_testdata/rat/gop.expect
@@ -0,0 +1,10 @@
+1 IDENT echo
+6 INT 1
+7 / 
+8 RAT 3r
+10 ; 
+
+11 IDENT echo
+16 RAT 1.23r
+21 ; 
+
diff --git a/tpl/scanner/_testdata/rat/in.gop b/tpl/scanner/_testdata/rat/in.gop
new file mode 100644
index 00000000..f008bdd9
--- /dev/null
+++ b/tpl/scanner/_testdata/rat/in.gop
@@ -0,0 +1,2 @@
+echo 1/3r
+echo 1.23r
diff --git a/tpl/scanner/_testdata/rat/tpl.expect b/tpl/scanner/_testdata/rat/tpl.expect
new file mode 100644
index 00000000..5f75eba6
--- /dev/null
+++ b/tpl/scanner/_testdata/rat/tpl.expect
@@ -0,0 +1,10 @@
+1 IDENT echo
+6 INT 1
+7 / 
+8 RAT 3r
+10 ; 
+
+11 IDENT echo
+16 RAT 1.23r
+21 ; 
+
diff --git a/tpl/scanner/_testdata/unit/go.expect b/tpl/scanner/_testdata/unit/go.expect
new file mode 100644
index 00000000..713110bb
--- /dev/null
+++ b/tpl/scanner/_testdata/unit/go.expect
@@ -0,0 +1,58 @@
+1 import import
+8 STRING "time"
+14 ; 
+
+16 IDENT x
+18 := 
+21 STRING "Let's wait for a second"
+46 ; 
+
+47 IDENT echo
+52 IDENT x
+53 ; 
+
+55 IDENT wait
+60 INT 0x10
+64 IDENT s
+65 ; 
+
+66 IDENT wait
+71 FLOAT .1
+73 IDENT s
+74 ; 
+
+75 IDENT wait
+80 FLOAT 1.
+82 IDENT s
+83 ; 
+
+84 IDENT wait
+89 FLOAT 1.e7
+93 IDENT s
+94 ; 
+
+95 IDENT wait
+100 FLOAT 1.2
+103 IDENT s
+104 ; 
+
+106 IDENT time
+110 . 
+111 IDENT sleep
+117 IMAG 100i
+121 IDENT m
+122 ; 
+
+124 IDENT spend
+130 FLOAT 10.0
+134 IDENT rmb
+137 ; 
+
+138 IDENT spend
+144 FLOAT 10.
+147 IDENT rmb
+150 ; 
+
+152 IDENT echo
+157 STRING "Hello, world"
+171 ... 
diff --git a/tpl/scanner/_testdata/unit/gop.expect b/tpl/scanner/_testdata/unit/gop.expect
new file mode 100644
index 00000000..1368f765
--- /dev/null
+++ b/tpl/scanner/_testdata/unit/gop.expect
@@ -0,0 +1,60 @@
+1 import import
+8 STRING "time"
+14 ; 
+
+16 IDENT x
+18 := 
+21 STRING "Let's wait for a second"
+46 ; 
+
+47 IDENT echo
+52 IDENT x
+53 ; 
+
+55 IDENT wait
+60 INT 0x10
+64 UNIT s
+65 ; 
+
+66 IDENT wait
+71 FLOAT .1
+73 UNIT s
+74 ; 
+
+75 IDENT wait
+80 FLOAT 1.
+82 UNIT s
+83 ; 
+
+84 IDENT wait
+89 FLOAT 1.e7
+93 UNIT s
+94 ; 
+
+95 IDENT wait
+100 FLOAT 1.2
+103 UNIT s
+104 ; 
+
+106 IDENT time
+110 . 
+111 IDENT sleep
+117 INT 100
+120 UNIT im
+122 ; 
+
+124 IDENT spend
+130 FLOAT 10.0
+134 UNIT rmb
+137 ; 
+
+138 IDENT spend
+144 FLOAT 10.
+147 UNIT rmb
+150 ; 
+
+152 IDENT echo
+157 STRING "Hello, world"
+171 ... 
+174 ; 
+
diff --git a/tpl/scanner/_testdata/unit/in.gop b/tpl/scanner/_testdata/unit/in.gop
new file mode 100644
index 00000000..38e5568e
--- /dev/null
+++ b/tpl/scanner/_testdata/unit/in.gop
@@ -0,0 +1,17 @@
+import "time"
+
+x := "Let's wait for a second"
+echo x
+
+wait 0x10s
+wait .1s
+wait 1.s
+wait 1.e7s
+wait 1.2s
+
+time.sleep 100im
+
+spend 10.0rmb
+spend 10.rmb
+
+echo "Hello, world"...
diff --git a/tpl/scanner/_testdata/unit/tpl.expect b/tpl/scanner/_testdata/unit/tpl.expect
new file mode 100644
index 00000000..3688b4d5
--- /dev/null
+++ b/tpl/scanner/_testdata/unit/tpl.expect
@@ -0,0 +1,58 @@
+1 IDENT import
+8 STRING "time"
+14 ; 
+
+16 IDENT x
+18 := 
+21 STRING "Let's wait for a second"
+46 ; 
+
+47 IDENT echo
+52 IDENT x
+53 ; 
+
+55 IDENT wait
+60 INT 0x10
+64 UNIT s
+65 ; 
+
+66 IDENT wait
+71 FLOAT .1
+73 UNIT s
+74 ; 
+
+75 IDENT wait
+80 FLOAT 1.
+82 UNIT s
+83 ; 
+
+84 IDENT wait
+89 FLOAT 1.e7
+93 UNIT s
+94 ; 
+
+95 IDENT wait
+100 FLOAT 1.2
+103 UNIT s
+104 ; 
+
+106 IDENT time
+110 . 
+111 IDENT sleep
+117 INT 100
+120 UNIT im
+122 ; 
+
+124 IDENT spend
+130 FLOAT 10.0
+134 UNIT rmb
+137 ; 
+
+138 IDENT spend
+144 FLOAT 10.
+147 UNIT rmb
+150 ; 
+
+152 IDENT echo
+157 STRING "Hello, world"
+171 ... 
diff --git a/tpl/scanner/scandir_test.go b/tpl/scanner/scandir_test.go
new file mode 100644
index 00000000..6d0eae31
--- /dev/null
+++ b/tpl/scanner/scandir_test.go
@@ -0,0 +1,82 @@
+/*
+ * 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 scanner_test
+
+import (
+	"bytes"
+	"io"
+	"log"
+	"os"
+	"path"
+	"strings"
+	"testing"
+
+	"github.com/goplus/gop/tpl/scanner/scannertest"
+)
+
+func testScan(
+	t *testing.T, pkgDir string, in []byte,
+	expFile string, scan func(w io.Writer, in []byte)) {
+	expect, _ := os.ReadFile(pkgDir + "/" + expFile)
+	var b bytes.Buffer
+	scan(&b, in)
+	out := b.Bytes()
+	if scannertest.Diff(t, pkgDir+"/result.txt", out, expect) {
+		t.Fatal(expFile, ": unexpect result")
+	}
+}
+
+func testFrom(t *testing.T, pkgDir, sel string) {
+	if sel != "" && !strings.Contains(pkgDir, sel) {
+		return
+	}
+	t.Helper()
+	log.Println("Scanning", pkgDir)
+	in, err := os.ReadFile(pkgDir + "/in.gop")
+	if err != nil {
+		t.Fatal("Scanning", pkgDir, "-", err)
+	}
+	testScan(t, pkgDir, in, "tpl.expect", scannertest.Scan)
+	testScan(t, pkgDir, in, "go.expect", scannertest.GoScan)
+	testScan(t, pkgDir, in, "gop.expect", scannertest.GopScan)
+}
+
+func testFromDir(t *testing.T, sel, relDir string) {
+	dir, err := os.Getwd()
+	if err != nil {
+		t.Fatal("Getwd failed:", err)
+	}
+	dir = path.Join(dir, relDir)
+	fis, err := os.ReadDir(dir)
+	if err != nil {
+		t.Fatal("ReadDir failed:", err)
+	}
+	for _, fi := range fis {
+		name := fi.Name()
+		if strings.HasPrefix(name, "_") {
+			continue
+		}
+		t.Run(name, func(t *testing.T) {
+			pkgDir := dir + "/" + name
+			testFrom(t, pkgDir, sel)
+		})
+	}
+}
+
+func TestFromTestdata(t *testing.T) {
+	testFromDir(t, "", "./_testdata")
+}
diff --git a/tpl/scanner/scannertest/scannertest.go b/tpl/scanner/scannertest/scannertest.go
new file mode 100644
index 00000000..4d9729bb
--- /dev/null
+++ b/tpl/scanner/scannertest/scannertest.go
@@ -0,0 +1,115 @@
+/*
+ * 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 scannertest
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"testing"
+
+	"github.com/goplus/gop/tpl/scanner"
+	"github.com/goplus/gop/tpl/token"
+
+	goscanner "go/scanner"
+	gotoken "go/token"
+
+	gopscanner "github.com/goplus/gop/scanner"
+	goptoken "github.com/goplus/gop/token"
+)
+
+// -----------------------------------------------------------------------------
+
+func Diff(t *testing.T, outfile string, dst, src []byte) bool {
+	line := 1
+	offs := 0 // line offset
+	for i := 0; i < len(dst) && i < len(src); i++ {
+		d := dst[i]
+		s := src[i]
+		if d != s {
+			os.WriteFile(outfile, dst, 0644)
+			t.Errorf("dst:%d: %s\n", line, dst[offs:])
+			t.Errorf("src:%d: %s\n", line, src[offs:])
+			return true
+		}
+		if s == '\n' {
+			line++
+			offs = i + 1
+		}
+	}
+	if len(dst) != len(src) {
+		os.WriteFile(outfile, dst, 0644)
+		t.Errorf("len(dst) = %d, len(src) = %d\ndst = %q\nsrc = %q", len(dst), len(src), dst, src)
+		return true
+	}
+	return false
+}
+
+// -----------------------------------------------------------------------------
+
+// Scan scans the input and writes the tokens to the writer.
+func Scan(w io.Writer, in []byte) {
+	var s scanner.Scanner
+	fset := gotoken.NewFileSet()
+	f := fset.AddFile("", -1, len(in))
+	s.Init(f, in, nil, scanner.ScanComments)
+	for {
+		t := s.Scan()
+		if t.Tok == token.EOF {
+			break
+		}
+		fmt.Fprintln(w, t.Pos, t.Tok, t.Lit)
+	}
+}
+
+// -----------------------------------------------------------------------------
+
+// GopScan scans the input using the Go+ standard library scanner and
+// writes the tokens to the writer.
+func GopScan(w io.Writer, in []byte) {
+	var s gopscanner.Scanner
+	fset := gotoken.NewFileSet()
+	f := fset.AddFile("", -1, len(in))
+	s.Init(f, in, nil, gopscanner.ScanComments)
+	for {
+		pos, tok, lit := s.Scan()
+		if tok == goptoken.EOF {
+			break
+		}
+		fmt.Fprintln(w, pos, tok, lit)
+	}
+}
+
+// -----------------------------------------------------------------------------
+
+// GoScan scans the input using the Go standard library scanner and
+// writes the tokens to the writer.
+func GoScan(w io.Writer, in []byte) {
+	var s goscanner.Scanner
+	fset := gotoken.NewFileSet()
+	f := fset.AddFile("", -1, len(in))
+	s.Init(f, in, nil, goscanner.ScanComments)
+	for {
+		pos, tok, lit := s.Scan()
+		if tok == gotoken.EOF {
+			break
+		}
+		fmt.Fprintln(w, pos, tok, lit)
+	}
+}
+
+// -----------------------------------------------------------------------------
diff --git a/tpl/token/token_test.go b/tpl/token/token_test.go
index 6478fe0f..1f214bea 100644
--- a/tpl/token/token_test.go
+++ b/tpl/token/token_test.go
@@ -26,7 +26,11 @@ func TestToken(t *testing.T) {
 		t.Fatal("literal_beg")
 	}
 	for i := Token(0); i < literal_end; i++ {
-		if i.String() != token.Token(i).String() {
+		s := i.String()
+		if s == "RAT" || s == "UNIT" || s == "CSTRING" || s == "PYSTRING" {
+			continue
+		}
+		if s != token.Token(i).String() {
 			t.Fatal("String:", i)
 		}
 	}
@@ -47,7 +51,14 @@ func TestToken(t *testing.T) {
 		count++
 		return 0
 	})
-	if count != 28 {
+	if count != 29 {
 		t.Fatal("ForEach:", count)
 	}
+	NewFileSet()
+	ForEach(0, func(tok Token, name string) int {
+		if tok != SHL {
+			t.Fatal("ForEach:", tok)
+		}
+		return Break
+	})
 }
diff --git a/x/typesutil/info_test.go b/x/typesutil/info_test.go
index 1cede6fa..ffcce113 100644
--- a/x/typesutil/info_test.go
+++ b/x/typesutil/info_test.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/goplus/gogen"
 	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/cl/cltest"
 	"github.com/goplus/gop/format"
 	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/token"
@@ -34,11 +35,13 @@ var spxProject = &modfile.Project{
 	PkgPaths: []string{"github.com/goplus/gop/cl/internal/spx", "math"}}
 
 var spxMod *gopmod.Module
+var gotypesalias bool
 
 func init() {
 	spxMod = gopmod.New(modload.Default)
 	spxMod.Opt.Projects = append(spxMod.Opt.Projects, spxProject)
 	spxMod.ImportClasses()
+	gotypesalias = cltest.EnableTypesalias()
 }
 
 func lookupClass(ext string) (c *modfile.Project, ok bool) {
@@ -105,7 +108,6 @@ func parseMixedSource(mod *gopmod.Module, fset *token.FileSet, name, src string,
 	err = check.Files(gofiles, []*ast.File{f})
 	return chkOpts.Types, info, ginfo, err
 }
-
 func parseSource(fset *token.FileSet, filename string, src any, mode parser.Mode) (*types.Package, *typesutil.Info, error) {
 	f, err := parser.ParseEntry(fset, filename, src, parser.Config{
 		Mode: mode,
@@ -116,7 +118,8 @@ func parseSource(fset *token.FileSet, filename string, src any, mode parser.Mode
 
 	pkg := types.NewPackage("", f.Name.Name)
 	conf := &types.Config{}
-	conf.Importer = importer.Default()
+	Gop := &env.Gop{Version: "1.0"}
+	conf.Importer = tool.NewImporter(nil, Gop, fset)
 	chkOpts := &typesutil.Config{
 		Types: pkg,
 		Fset:  fset,
@@ -1484,14 +1487,49 @@ func (p *N) Test__1(n int) {
 }
 
 func TestOverloadNamed(t *testing.T) {
-	testGopInfo(t, `
-import "github.com/goplus/gop/cl/internal/overload/bar"
-
-var a bar.Var[int]
-var b bar.Var[bar.M]
-c := bar.Var(string)
-d := bar.Var(bar.M)
-`, ``, `== types ==
+	var expect string
+	if gotypesalias {
+		expect = `== types ==
+000:  4: 7 | bar.Var             *ast.SelectorExpr              | type    : github.com/goplus/gop/cl/internal/overload/bar.Var__0[int] | type
+001:  4: 7 | bar.Var[int]        *ast.IndexExpr                 | type    : github.com/goplus/gop/cl/internal/overload/bar.Var__0[int] | type
+002:  4:15 | int                 *ast.Ident                     | type    : int | type
+003:  5: 7 | bar.Var             *ast.SelectorExpr              | type    : github.com/goplus/gop/cl/internal/overload/bar.Var__1[github.com/goplus/gop/cl/internal/overload/bar.M] | type
+004:  5: 7 | bar.Var[bar.M]      *ast.IndexExpr                 | type    : github.com/goplus/gop/cl/internal/overload/bar.Var__1[github.com/goplus/gop/cl/internal/overload/bar.M] | type
+005:  5:15 | bar.M               *ast.SelectorExpr              | type    : github.com/goplus/gop/cl/internal/overload/bar.M | type
+006:  6: 6 | bar.Var             *ast.SelectorExpr              | value   : func[T github.com/goplus/gop/cl/internal/overload/bar.basetype]() *github.com/goplus/gop/cl/internal/overload/bar.Var__0[T] | value
+007:  6: 6 | bar.Var(string)     *ast.CallExpr                  | value   : *github.com/goplus/gop/cl/internal/overload/bar.Var__0[string] | value
+008:  6:14 | string              *ast.Ident                     | type    : string | type
+009:  7: 6 | bar.Var             *ast.SelectorExpr              | value   : func[T map[string]any]() *github.com/goplus/gop/cl/internal/overload/bar.Var__1[T] | value
+010:  7: 6 | bar.Var(bar.M)      *ast.CallExpr                  | value   : *github.com/goplus/gop/cl/internal/overload/bar.Var__1[github.com/goplus/gop/cl/internal/overload/bar.M] | value
+011:  7:14 | bar.M               *ast.SelectorExpr              | var     : github.com/goplus/gop/cl/internal/overload/bar.M | variable
+== defs ==
+000:  4: 5 | a                   | var main.a github.com/goplus/gop/cl/internal/overload/bar.Var__0[int]
+001:  5: 5 | b                   | var main.b github.com/goplus/gop/cl/internal/overload/bar.Var__1[github.com/goplus/gop/cl/internal/overload/bar.M]
+002:  6: 1 | c                   | var c *github.com/goplus/gop/cl/internal/overload/bar.Var__0[string]
+003:  6: 1 | main                | func main.main()
+004:  7: 1 | d                   | var d *github.com/goplus/gop/cl/internal/overload/bar.Var__1[github.com/goplus/gop/cl/internal/overload/bar.M]
+== uses ==
+000:  4: 7 | bar                 | package bar ("github.com/goplus/gop/cl/internal/overload/bar")
+001:  4:11 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var__0[T github.com/goplus/gop/cl/internal/overload/bar.basetype] struct{val T}
+002:  4:15 | int                 | type int
+003:  5: 7 | bar                 | package bar ("github.com/goplus/gop/cl/internal/overload/bar")
+004:  5:11 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var__1[T map[string]any] struct{val T}
+005:  5:15 | bar                 | package bar ("github.com/goplus/gop/cl/internal/overload/bar")
+006:  5:19 | M                   | type github.com/goplus/gop/cl/internal/overload/bar.M = map[string]any
+007:  6: 6 | bar                 | package bar ("github.com/goplus/gop/cl/internal/overload/bar")
+008:  6:10 | Var                 | func github.com/goplus/gop/cl/internal/overload/bar.Gopx_Var_Cast__0[T github.com/goplus/gop/cl/internal/overload/bar.basetype]() *github.com/goplus/gop/cl/internal/overload/bar.Var__0[T]
+009:  6:14 | string              | type string
+010:  7: 6 | bar                 | package bar ("github.com/goplus/gop/cl/internal/overload/bar")
+011:  7:10 | Var                 | func github.com/goplus/gop/cl/internal/overload/bar.Gopx_Var_Cast__1[T map[string]any]() *github.com/goplus/gop/cl/internal/overload/bar.Var__1[T]
+012:  7:14 | bar                 | package bar ("github.com/goplus/gop/cl/internal/overload/bar")
+013:  7:18 | M                   | type github.com/goplus/gop/cl/internal/overload/bar.M = map[string]any
+== overloads ==
+000:  4:11 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})
+001:  5:11 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})
+002:  6:10 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})
+003:  7:10 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})`
+	} else {
+		expect = `== types ==
 000:  4: 7 | bar.Var             *ast.SelectorExpr              | type    : github.com/goplus/gop/cl/internal/overload/bar.Var__0[int] | type
 001:  4: 7 | bar.Var[int]        *ast.IndexExpr                 | type    : github.com/goplus/gop/cl/internal/overload/bar.Var__0[int] | type
 002:  4:15 | int                 *ast.Ident                     | type    : int | type
@@ -1529,7 +1567,16 @@ d := bar.Var(bar.M)
 000:  4:11 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})
 001:  5:11 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})
 002:  6:10 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})
-003:  7:10 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})`)
+003:  7:10 | Var                 | type github.com/goplus/gop/cl/internal/overload/bar.Var = func(__gop_overload_args__ interface{_()})`
+	}
+	testGopInfo(t, `
+import "github.com/goplus/gop/cl/internal/overload/bar"
+
+var a bar.Var[int]
+var b bar.Var[bar.M]
+c := bar.Var(string)
+d := bar.Var(bar.M)
+`, ``, expect)
 }
 
 func TestMixedOverloadNamed(t *testing.T) {
@@ -2444,3 +2491,18 @@ func add = (
 013: 13:14 | b                   | var b float64
 014: 15: 2 | addString           | func (*main.Rect).addString(a string, b string) string`)
 }
+
+func TestTypesAlias(t *testing.T) {
+	testInfo(t, `package main
+import "fmt"
+
+type T = int
+var v T
+func demo(v T) {
+	fmt.Println(v)
+}
+func main() {
+	demo(100)
+}
+`)
+}