Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cl/_testgop/kwargs5/in.xgo
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
type CompletionParams interface {
MaxOutputTokens(int64) CompletionParams
Temperature(float64) CompletionParams
}

type completionParamsImpl struct {
maxTokens int64
temperature float64
}

func (p *completionParamsImpl) MaxOutputTokens(n int64) CompletionParams {
p.maxTokens = n
return p
}

func (p *completionParamsImpl) Temperature(t float64) CompletionParams {
p.temperature = t
return p
}

type Client struct{}

func (c *Client) CompletionParams() CompletionParams {
return &completionParamsImpl{}
}

func (c *Client) Complete(prompt string, params CompletionParams) {
}

var c Client
c.Complete "hello", maxOutputTokens = 1024, temperature = 0.7
32 changes: 32 additions & 0 deletions cl/_testgop/kwargs5/out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

type CompletionParams interface {
MaxOutputTokens(int64) CompletionParams
Temperature(float64) CompletionParams
}
type completionParamsImpl struct {
maxTokens int64
temperature float64
}
type Client struct {
}

func (p *completionParamsImpl) MaxOutputTokens(n int64) CompletionParams {
p.maxTokens = n
return p
}
func (p *completionParamsImpl) Temperature(t float64) CompletionParams {
p.temperature = t
return p
}
func (c *Client) CompletionParams() CompletionParams {
return &completionParamsImpl{}
}
func (c *Client) Complete(prompt string, params CompletionParams) {
}

var c Client

func main() {
c.Complete("hello", c.CompletionParams().MaxOutputTokens(1024).Temperature(0.7))
}
38 changes: 38 additions & 0 deletions cl/_testgop/kwargs6/in.xgo
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type CompletionParams interface {
Set(name string, val any) CompletionParams
MaxOutputTokens(int64) CompletionParams
System(prompt ...string) CompletionParams
}

type completionParamsImpl struct {
data map[string]any
}

func (p *completionParamsImpl) Set(name string, val any) CompletionParams {
p.data[name] = val
return p
}

func (p *completionParamsImpl) MaxOutputTokens(n int64) CompletionParams {
p.data["maxOutputTokens"] = n
return p
}

func (p *completionParamsImpl) System(prompt ...string) CompletionParams {
p.data["system"] = prompt
return p
}

type Client struct{}

func (c *Client) CompletionParams() CompletionParams {
return &completionParamsImpl{data: make(map[string]any)}
}

func (c *Client) Complete(prompt string, params CompletionParams) {
}

var c Client

// Known typed keywords + unknown keyword falling back to Set
c.Complete "hello", maxOutputTokens = 1024, system = "You are helpful.", topP = 0.9
36 changes: 36 additions & 0 deletions cl/_testgop/kwargs6/out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

type CompletionParams interface {
MaxOutputTokens(int64) CompletionParams
Set(name string, val interface{}) CompletionParams
System(prompt ...string) CompletionParams
}
type completionParamsImpl struct {
data map[string]interface{}
}
type Client struct {
}

func (p *completionParamsImpl) Set(name string, val interface{}) CompletionParams {
p.data[name] = val
return p
}
func (p *completionParamsImpl) MaxOutputTokens(n int64) CompletionParams {
p.data["maxOutputTokens"] = n
return p
}
func (p *completionParamsImpl) System(prompt ...string) CompletionParams {
p.data["system"] = prompt
return p
}
func (c *Client) CompletionParams() CompletionParams {
return &completionParamsImpl{data: make(map[string]interface{})}
}
func (c *Client) Complete(prompt string, params CompletionParams) {
}

var c Client
// Known typed keywords + unknown keyword falling back to Set
func main() {
c.Complete("hello", c.CompletionParams().MaxOutputTokens(1024).System("You are helpful.").Set("topP", 0.9))
}
32 changes: 32 additions & 0 deletions cl/error_msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1148,3 +1148,35 @@ var a = struct{v int}{(x => x)}
var a = struct{v int}{v: (x => x)}
`)
}

func TestErrInterfaceKwargsNoSet(t *testing.T) {
codeErrorTest(t, `bar.xgo:14:39: interface Params does not support unknown keyword "topP"`, `
type Params interface {
MaxTokens(int64) Params
}

type paramsImpl struct{}
func (p *paramsImpl) MaxTokens(n int64) Params { return p }

type Client struct{}
func (c *Client) Params() Params { return &paramsImpl{} }
func (c *Client) Complete(prompt string, params Params) {}

var c Client
c.Complete "hello", maxTokens = 1024, topP = 0.9
`)
}

func TestErrInterfaceKwargsNoFactory(t *testing.T) {
codeErrorTest(t, `bar.xgo:10:1: c.Complete "hello", maxTok undefined (type Client has no field or method Params)`, `
type Params interface {
MaxTokens(int64) Params
}

type Client struct{}
func (c *Client) Complete(prompt string, params Params) {}

var c Client
c.Complete "hello", maxTokens = 1024
`)
}
111 changes: 110 additions & 1 deletion cl/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,9 @@ func compileCallExpr(ctx *blockCtx, lhs int, v *ast.CallExpr, inFlags int) {
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"
msgUnexpectedKwargs = "keyword arguments can only be used for struct, map[string]T or interface types, but got %v"
msgIfaceNoMatchKeyword = "interface %v does not support unknown keyword %q"
msgIfaceNeedReceiver = "interface-based keyword arguments require a method call with a receiver, but got %v"
)

func inThisPkg(ctx *blockCtx, t types.Type) bool {
Expand All @@ -944,6 +946,8 @@ func mergeKwargs(ctx *blockCtx, v *ast.CallExpr, t types.Type) ast.Expr {
panic(ctx.newCodeErrorf(v.Pos(), v.End(), msgUnexpectedKwargs, t))
case *types.Struct:
return mergeStructKwargs(v.Kwargs, u, inThisPkg(ctx, t))
case *types.Interface:
return mergeInterfaceKwargs(ctx, v, t, u)
}
}
return mergeStringMapKwargs(v.Kwargs) // fallback to map[string]T
Expand Down Expand Up @@ -1001,6 +1005,111 @@ func getFldName(name *ast.Ident, u *types.Struct, inPkg bool) *ast.Ident {
return name // fallback to origin name
}

// mergeInterfaceKwargs synthesizes a builder-pattern method chain for interface-based
// keyword arguments. Given kwargs like `maxOutputTokens = 1024, system = "hello"`,
// it produces an AST equivalent to:
//
// receiver.InterfaceName().MaxOutputTokens(1024).System("hello")
//
// See https://github.com/goplus/xgo/issues/2678
func mergeInterfaceKwargs(ctx *blockCtx, v *ast.CallExpr, t types.Type, iface *types.Interface) ast.Expr {
named, ok := t.(*types.Named)
if !ok {
panic(ctx.newCodeErrorf(v.Pos(), v.End(), msgUnexpectedKwargs, t))
}
ifaceName := named.Obj().Name()

se, ok := v.Fun.(*ast.SelectorExpr)
if !ok {
panic(ctx.newCodeErrorf(v.Pos(), v.End(), msgIfaceNeedReceiver, ifaceName))
}

hasSet := ifaceHasSetMethod(iface, t)

// Build the factory call: receiver.InterfaceName()
pos := v.Kwargs[0].Pos()
var chain ast.Expr = &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: se.X,
Sel: &ast.Ident{NamePos: pos, Name: ifaceName},
},
}

for _, kwarg := range v.Kwargs {
methodName := findIfaceMethod(iface, kwarg.Name.Name)
if methodName != "" {
chain = &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: chain,
Sel: &ast.Ident{NamePos: kwarg.Name.NamePos, Name: methodName},
},
Args: []ast.Expr{kwarg.Value},
}
} else if hasSet {
chain = &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: chain,
Sel: &ast.Ident{NamePos: kwarg.Name.NamePos, Name: "Set"},
},
Args: []ast.Expr{
&ast.BasicLit{ValuePos: kwarg.Name.NamePos, Kind: token.STRING, Value: strconv.Quote(kwarg.Name.Name)},
kwarg.Value,
},
}
} else {
panic(ctx.newCodeErrorf(kwarg.Pos(), kwarg.End(), msgIfaceNoMatchKeyword, t, kwarg.Name.Name))
}
}
Comment on lines +1038 to +1062

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: No duplicate keyword detection — if the same keyword appears twice (e.g., maxTokens = 1024, maxTokens = 2048), both calls are chained silently. The struct kwargs path would be caught by Go's duplicate field check, but the interface path succeeds quietly. This is almost certainly a programmer mistake. Consider detecting and erroring on duplicates.


return chain
}

// ifaceHasSetMethod reports whether iface has a Set(string, any) Self method.
func ifaceHasSetMethod(iface *types.Interface, self types.Type) bool {
for i := 0; i < iface.NumMethods(); i++ {
m := iface.Method(i)
if m.Name() != "Set" {
continue
}
sig, ok := m.Type().(*types.Signature)
if !ok || sig.Params().Len() != 2 || sig.Results().Len() != 1 {
continue
}
if isStringType(sig.Params().At(0).Type()) &&
isAnyType(sig.Params().At(1).Type()) &&
types.Identical(sig.Results().At(0).Type(), self) {
return true
}
}
return false
}

// findIfaceMethod finds a method on the interface matching kwName (case-insensitive).
// Returns the actual method name if found, or "" if not.
func findIfaceMethod(iface *types.Interface, kwName string) string {
kwLower := strings.ToLower(kwName)
for i := 0; i < iface.NumMethods(); i++ {
m := iface.Method(i)
if m.Name() == "Set" {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The Set exclusion here is case-sensitive (m.Name() == "Set"), but keyword-to-method matching below is case-insensitive. This means a keyword set = value would case-insensitively match the Set method here (not skipped since "Set" != "Set" — wait, it would be skipped). Actually the real concern is the reverse: if a user writes set = 42, findIfaceMethod skips Set, returns "", and falls through to the hasSet fallback which calls Set("set", 42). This is probably the desired behavior, but it means Set can never be called as a typed single-arg method via kwargs. Worth a brief comment documenting this intentional reservation of the Set name.

continue
}
if strings.ToLower(m.Name()) == kwLower {
return m.Name()
}
}
return ""
}
Comment on lines +1089 to +1101

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two concerns with findIfaceMethod:

1. Ambiguous case-insensitive matching: Returns the first method whose lowered name matches. If an interface has two methods differing only in casing (e.g., TopP and Topp), the result depends on iteration order with no warning. Consider preferring an exact match first, or detecting and reporting ambiguity.

2. No signature validation: Matches purely by name and the call site always emits exactly one argument. A getter-style method like Temperature() float64 would match keyword temperature = 0.7 and produce chain.Temperature(0.7), which fails downstream with a confusing arity error instead of a clear kwargs-specific message. Consider filtering to methods with at least one parameter that return the interface type (Self).


func isStringType(t types.Type) bool {
basic, ok := t.(*types.Basic)
return ok && basic.Kind() == types.String
}

func isAnyType(t types.Type) bool {
iface, ok := t.(*types.Interface)
return ok && iface.Empty()
}

func toBasicLit(fn *ast.Ident) *ast.BasicLit {
return &ast.BasicLit{ValuePos: fn.NamePos, Kind: token.STRING, Value: strconv.Quote(fn.Name)}
}
Expand Down