Repository: goplus/xgo

Issue:
keyword arguments (Python-like kwargs syntax)
### Proposal

In Python, `**kwargs` is a special syntax for handling keyword arguments. It allows a function to accept any number of keyword arguments (i.e., arguments in the form of `key=value`) and stores these arguments in a dictionary. `**kwargs` must be placed after all other parameters, including `*args` (if present).

kwargs can significantly improve code readability when a function has many parameters, and it shares similarities with command-line switches (while XGo advocates for a command-line style syntax in writing code), hence it has certain support value.

How to support kwargs? Our principle is to keep changes as minimal as possible.

For the following Python function:

```python
def f(*args, **kwargs)
```

In XGo and Go, we define it with the following syntax:

```go
func f(kwargs map[string]any, args ...any)
```

When using it, the syntax in XGo and Python is the same, both are:

```python
f(name="Ken", age=17)
```

What are the new syntaxes?

* When calling a function, use `key=expr` to represent named parameters. Named parameters must be placed after other parameters when calling the function.

* When defining a function, the kwargs parameter must be the last parameter (if there are no variadic parameters) or the second-to-last parameter (if there are variadic parameters).

Additionally, it should be emphasized that the kwargs parameter can also be a struct or struct pointer besides using a map. For example:

```go
type Options struct {
    Name string
    Age int
}

func f(opts *Options, args ...any)
```

We can still use the kwargs parameter calling syntax (note that parameter names can start with either lowercase or uppercase letters):

```python
f(name="Ken", age=17)
f(Name="Ken", Age=17)
```


Hints from issue comments:
@xushiwei: I've noticed an issue: if the kwargs feature is not an optional parameter, for example, calling f() in the above case would result in an "not enough arguments" error. Because of this, we introduced `T?` to represent optional parameters (see #2449 for details).

```go
func f(opts map[string]any?, args ...any)
func f(opts *Options?, args ...any)
```
@xushiwei: @niupilot Please add documentation related to keyword arguments to the quick start.@niupilot: ### Documentation Added Successfully ✓

I've added comprehensive documentation for the keyword arguments feature to the quick start guide.

#### What was added:

**New Section: "Keyword arguments"**
- Located after "Variadic parameters" section in the Functions chapter
- Includes three detailed subsections:
  1. **Using kwargs with maps** - Shows `map[string]any?` usage with practical examples
  2. **Using kwargs with structs** - Demonstrates type-safe kwargs with struct types
  3. **Combining with variadic parameters** - Shows how to use kwargs alongside `...args`

**Key features covered:**
- ✓ Python-like `key=value` call syntax
- ✓ Optional parameters using `T?` syntax
- ✓ Case-insensitive field name matching (lowercase/uppercase)
- ✓ Integration with variadic parameters
- ✓ Clear rules and best practices

**Table of Contents updated** to include the new section link.

#### Pull Request
Created PR #2456: https://github.com/goplus/xgo/pull/2456

---
Branch: [`codeagent/claude/issue-2443-1760147565`](https://github.com/goplus/xgo/tree/codeagent/claude/issue-2443-1760147565)

Solution patch:
diff --git a/cl/_testgop/kwargs1/in.xgo b/cl/_testgop/kwargs1/in.xgo
new file mode 100644
index 00000000..c76e46d2
--- /dev/null
+++ b/cl/_testgop/kwargs1/in.xgo
@@ -0,0 +1,10 @@
+type Options struct {
+    Loop  bool
+    async bool
+}
+
+func PlaySound(path string, options *Options) {
+}
+
+playSound "1.mp3", loop = true
+playSound "2.mp3", loop = false, async = true
diff --git a/cl/_testgop/kwargs2/in.xgo b/cl/_testgop/kwargs2/in.xgo
new file mode 100644
index 00000000..84401627
--- /dev/null
+++ b/cl/_testgop/kwargs2/in.xgo
@@ -0,0 +1,10 @@
+type Options struct {
+    Loop  bool
+    async bool
+}
+
+func PlaySound(path string, options Options) {
+}
+
+playSound "1.mp3", loop = true
+playSound "2.mp3", loop = false, async = true
diff --git a/cl/_testgop/kwargs3/in.xgo b/cl/_testgop/kwargs3/in.xgo
new file mode 100644
index 00000000..1f310def
--- /dev/null
+++ b/cl/_testgop/kwargs3/in.xgo
@@ -0,0 +1,7 @@
+type Options map[string]bool
+
+func PlaySound(options Options, paths ...string) {
+}
+
+playSound "1.mp3", "foo.wav", loop = false
+playSound "2.mp3", loop = true, async = true
diff --git a/cl/_testgop/kwargs4/in.xgo b/cl/_testgop/kwargs4/in.xgo
new file mode 100644
index 00000000..a6fc3c27
--- /dev/null
+++ b/cl/_testgop/kwargs4/in.xgo
@@ -0,0 +1,7 @@
+import "github.com/goplus/xgo/cl/internal/testutil"
+
+func PlaySound(path string, options *testutil.Options) {
+}
+
+playSound "1.mp3", loop = true
+playSound "2.mp3", Loop = false, async = true
diff --git a/cl/expr.go b/cl/expr.go
index ad9283b8..771d90a8 100644
--- a/cl/expr.go
+++ b/cl/expr.go
@@ -33,6 +33,7 @@ import (
 	"github.com/goplus/xgo/printer"
 	"github.com/goplus/xgo/token"
 	tpl "github.com/goplus/xgo/tpl/ast"
+	"github.com/qiniu/x/stringutil"
 )
 
 /*-----------------------------------------------------------------------------
@@ -712,6 +713,28 @@ func compileCallExpr(ctx *blockCtx, v *ast.CallExpr, inFlags int) {
 	fnt := pfn.Type
 	fn := &fnType{}
 	fn.load(fnt)
+	if len(v.Kwargs) > 0 { // https://github.com/goplus/xgo/issues/2443
+		n := len(v.Args)
+		args := make([]ast.Expr, n+1)
+		if fn.variadic { // has variadic parameter
+			idx := fn.size - 1
+			if idx < 0 {
+				panic(ctx.newCodeError(v.Pos(), v.End(), msgNoKwargsOVF))
+			}
+			if len(v.Args) < idx {
+				panic(ctx.newCodeError(v.Pos(), v.End(), msgNoEnoughArgToKwargs))
+			}
+			copy(args, v.Args[:idx])
+			args[idx] = mergeKwargs(ctx, v, fn.params.At(idx).Type())
+			copy(args[idx+1:], v.Args[idx:])
+		} else {
+			copy(args, v.Args)
+			args[n] = mergeKwargs(ctx, v, fn.arg(n, false))
+		}
+		ne := *v
+		ne.Args, ne.Kwargs = args, nil
+		v = &ne
+	}
 	for fn != nil {
 		if err = compileCallArgs(ctx, pfn, fn, v, ellipsis, flags); err == nil {
 			if rec := ctx.recorder(); rec != nil {
@@ -728,6 +751,89 @@ func compileCallExpr(ctx *blockCtx, v *ast.CallExpr, inFlags int) {
 	panic(err)
 }
 
+const (
+	msgNoKwargsOVF         = "keyword arguments are not supported for a function with only variadic parameters"
+	msgNoEnoughArgToKwargs = "not enough arguments for function call with keyword arguments"
+	msgUnexpectedKwargs    = "keyword arguments can only be used for struct or map[string]T types, but got %v"
+)
+
+func inThisPkg(ctx *blockCtx, t types.Type) bool {
+	if named, ok := t.(*types.Named); ok {
+		if named.Obj().Pkg() == ctx.pkg.Types {
+			return true
+		}
+	}
+	return false
+}
+
+func mergeKwargs(ctx *blockCtx, v *ast.CallExpr, t types.Type) ast.Expr {
+	if t != nil {
+		switch u := t.Underlying().(type) {
+		case *types.Pointer:
+			t = u.Elem()
+			if u, ok := t.Underlying().(*types.Struct); ok {
+				return mergeStructKwargs(v.Kwargs, u, inThisPkg(ctx, t))
+			}
+			panic(ctx.newCodeErrorf(v.Pos(), v.End(), msgUnexpectedKwargs, t))
+		case *types.Struct:
+			return mergeStructKwargs(v.Kwargs, u, inThisPkg(ctx, t))
+		}
+	}
+	return mergeStringMapKwargs(v.Kwargs) // fallback to map[string]T
+}
+
+func mergeStringMapKwargs(kwargs []*ast.KwargExpr) ast.Expr {
+	n := len(kwargs)
+	elts := make([]ast.Expr, n)
+	for i, arg := range kwargs {
+		elts[i] = &ast.KeyValueExpr{
+			Key:   toBasicLit(arg.Name),
+			Value: arg.Value,
+		}
+	}
+	return &ast.CompositeLit{
+		Lbrace: kwargs[0].Pos() - 1,
+		Elts:   elts,
+		Rbrace: kwargs[n-1].End(),
+	}
+}
+
+func mergeStructKwargs(kwargs []*ast.KwargExpr, u *types.Struct, inPkg bool) ast.Expr {
+	n := len(kwargs)
+	elts := make([]ast.Expr, n)
+	for i, arg := range kwargs {
+		elts[i] = &ast.KeyValueExpr{
+			Key:   getFldName(arg.Name, u, inPkg),
+			Value: arg.Value,
+		}
+	}
+	return &ast.CompositeLit{
+		Lbrace: kwargs[0].Pos() - 1,
+		Elts:   elts,
+		Rbrace: kwargs[n-1].End(),
+	}
+}
+
+func getFldName(name *ast.Ident, u *types.Struct, inPkg bool) *ast.Ident {
+	if name.IsExported() {
+		return name
+	}
+	capName := stringutil.Capitalize(name.Name)
+	if !inPkg {
+		return &ast.Ident{NamePos: name.NamePos, Name: capName}
+	}
+	for i, n := 0, u.NumFields(); i < n; i++ {
+		fld := u.Field(i)
+		if fld.Name() == name.Name {
+			return name
+		}
+		if fld.Exported() && fld.Name() == capName {
+			return &ast.Ident{NamePos: name.NamePos, Name: capName}
+		}
+	}
+	return name // fallback to origin name
+}
+
 func toBasicLit(fn *ast.Ident) *ast.BasicLit {
 	return &ast.BasicLit{ValuePos: fn.NamePos, Kind: token.STRING, Value: strconv.Quote(fn.Name)}
 }



Test patch:
diff --git a/cl/_testgop/kwargs1/out.go b/cl/_testgop/kwargs1/out.go
new file mode 100644
index 00000000..a215efd7
--- /dev/null
+++ b/cl/_testgop/kwargs1/out.go
@@ -0,0 +1,13 @@
+package main
+
+type Options struct {
+	Loop  bool
+	async bool
+}
+
+func PlaySound(path string, options *Options) {
+}
+func main() {
+	PlaySound("1.mp3", &Options{Loop: true})
+	PlaySound("2.mp3", &Options{Loop: false, async: true})
+}
diff --git a/cl/_testgop/kwargs2/out.go b/cl/_testgop/kwargs2/out.go
new file mode 100644
index 00000000..b4e7a2b9
--- /dev/null
+++ b/cl/_testgop/kwargs2/out.go
@@ -0,0 +1,13 @@
+package main
+
+type Options struct {
+	Loop  bool
+	async bool
+}
+
+func PlaySound(path string, options Options) {
+}
+func main() {
+	PlaySound("1.mp3", Options{Loop: true})
+	PlaySound("2.mp3", Options{Loop: false, async: true})
+}
diff --git a/cl/_testgop/kwargs3/out.go b/cl/_testgop/kwargs3/out.go
new file mode 100644
index 00000000..3bae5358
--- /dev/null
+++ b/cl/_testgop/kwargs3/out.go
@@ -0,0 +1,10 @@
+package main
+
+type Options map[string]bool
+
+func PlaySound(options Options, paths ...string) {
+}
+func main() {
+	PlaySound(Options{"loop": false}, "1.mp3", "foo.wav")
+	PlaySound(Options{"loop": true, "async": true}, "2.mp3")
+}
diff --git a/cl/_testgop/kwargs4/out.go b/cl/_testgop/kwargs4/out.go
new file mode 100644
index 00000000..2cfa2af7
--- /dev/null
+++ b/cl/_testgop/kwargs4/out.go
@@ -0,0 +1,10 @@
+package main
+
+import "github.com/goplus/xgo/cl/internal/testutil"
+
+func PlaySound(path string, options *testutil.Options) {
+}
+func main() {
+	PlaySound("1.mp3", &testutil.Options{Loop: true})
+	PlaySound("2.mp3", &testutil.Options{Loop: false, Async: true})
+}
diff --git a/cl/internal/testutil/testutil.go b/cl/internal/testutil/testutil.go
new file mode 100644
index 00000000..14b7c169
--- /dev/null
+++ b/cl/internal/testutil/testutil.go
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 The XGo Authors (xgo.dev). 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 testutil
+
+type Options struct {
+	Loop  bool
+	Async bool
+}