Repository navigation
cl: implement interface-based keyword arguments #2679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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)) | ||
| } |
| 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 |
| 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)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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 | ||
|
|
@@ -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)) | ||
| } | ||
| } | ||
|
|
||
| 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" { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: The |
||
| continue | ||
| } | ||
| if strings.ToLower(m.Name()) == kwLower { | ||
| return m.Name() | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
Comment on lines
+1089
to
+1101
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two concerns with 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., 2. No signature validation: Matches purely by name and the call site always emits exactly one argument. A getter-style method like |
||
|
|
||
| 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)} | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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.