Repository: goplus/gop
Pull request #2239 (2025-04-01 04:22:41)

minispec: SendStmt, CommandStmt; tpl: WhiteSpace


Patch:
diff --git a/demo/gop-calc/calc.gop b/demo/gop-calc/calc.gop
index d9bd0cc8..59b28311 100644
--- a/demo/gop-calc/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-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/lambda1/lambda.gop b/demo/lambda1/lambda.gop
index 2d9d25b1..28f88848 100644
--- a/demo/lambda1/lambda.gop
+++ b/demo/lambda1/lambda.gop
@@ -6,4 +6,3 @@ echo f(1.0, x => 2 * x)
 echo f(5.0, (x) => {
     return 2 * x
 })
-
diff --git a/doc/spec/mini/mini.gop b/doc/spec/mini/mini.gop
index 49e9426f..ff2e24c2 100644
--- a/doc/spec/mini/mini.gop
+++ b/doc/spec/mini/mini.gop
@@ -97,7 +97,7 @@ IfStmt = "if" ?(SimpleStmt ";") Expression Block ?("else" (IfStmt | Block))
 
 SwitchStmt = "switch" ?(SimpleStmt ";") SwitchGuard "{" *CaseClause "}"
 
-SwitchGuard = ?(IDENT ":=") PrimaryExpr "." "(" "type" ")" | ?Expression
+SwitchGuard = ?(IDENT ":=") PrimaryExpr | ?Expression
 
 CaseClause = ("case" ExpressionList | "default") ":" StatementList
 
@@ -107,7 +107,9 @@ DeferStmt = "defer" Expression
 
 LabeledStmt = IDENT ":" Statement
 
-SimpleStmt = ShortVarDecl | IncDecStmt | Assignment | ExpressionStmt | EmptyStmt
+SimpleStmt = SendStmt | ShortVarDecl | IncDecStmt | Assignment | ExpressionStmt | EmptyStmt
+
+SendStmt = IDENT ?("." IDENT) "<-" LambdaExprList
 
 ShortVarDecl = IdentifierList ":=" ExpressionList
 
@@ -119,7 +121,7 @@ Assignment = ExpressionList (
 
 ExpressionStmt = Expression
 
-CommandStmt = IDENT ?("." IDENT) LambdaExprList
+CommandStmt = IDENT ?("." IDENT) SPACE LambdaExprList
 
 EmptyStmt = ""
 
diff --git a/tpl/cl/compile.go b/tpl/cl/compile.go
index 12d0f770..97d6e2bf 100644
--- a/tpl/cl/compile.go
+++ b/tpl/cl/compile.go
@@ -194,6 +194,8 @@ func compileExpr(expr ast.Expr, ctx *context) (matcher.Matcher, bool) {
 			quoteCh = '`'
 		case "QSTRING":
 			quoteCh = '"'
+		case "SPACE":
+			return matcher.WhiteSpace(), true
 		default:
 			ctx.addErrorf(expr.Pos(), "`%s` is undefined", name)
 		}
diff --git a/tpl/matcher/match.go b/tpl/matcher/match.go
index 76f51d95..2ada9480 100644
--- a/tpl/matcher/match.go
+++ b/tpl/matcher/match.go
@@ -29,6 +29,7 @@ var (
 	// ErrVarAssigned error
 	ErrVarAssigned = errors.New("variable is already assigned")
 
+	errNoWhitespace  = errors.New("no whitespace")
 	errAdjoinEmpty   = errors.New("adjoin empty")
 	errMultiMismatch = errors.New("multiple mismatch")
 )
@@ -89,17 +90,19 @@ func (e RecursiveError) Error() string {
 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),
 	}
 }
 
@@ -226,6 +229,33 @@ func True() Matcher {
 
 // -----------------------------------------------------------------------------
 
+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) {
diff --git a/tpl/tpl.go b/tpl/tpl.go
index 2bf061b4..a9dc15eb 100644
--- a/tpl/tpl.go
+++ b/tpl/tpl.go
@@ -253,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 {


Test patch:
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")
 	}
 }
+
+// -----------------------------------------------------------------------------