Skip to content

cl: implement interface-based keyword arguments - #2679

Merged
xushiwei merged 1 commit into
mainfrom
xgopilot/issue-2678-1774232806
Mar 23, 2026
Merged

xushiwei merged 1 commit into
mainfrom
xgopilot/issue-2678-1774232806

Conversation

@fennoai

@fennoai fennoai Bot commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Requested by @xushiwei

Closes #2678

Summary

  • Add support for interface-based keyword arguments in the cl package that desugar into builder-pattern method chains
  • When a function's last parameter is a named interface type and the receiver has a factory method matching the interface name, keyword arguments are synthesized as chained method calls
  • Case-insensitive keyword-to-method matching with optional Set(string, any) fallback for unknown keywords
  • Compile-time errors for unknown keywords when no Set method exists on the interface

Desugaring Example

c.Complete ctx, maxOutputTokens = 1024, system = "hello", topP = 0.9

Desugars to:

c.Complete(ctx, c.CompletionParams().MaxOutputTokens(1024).System("hello").Set("topP", 0.9))

Changes

  • cl/expr.go: Extended mergeKwargs to handle *types.Interface, added mergeInterfaceKwargs and helper functions
  • cl/_testgop/kwargs5/: Test case for basic interface kwargs (typed methods only)
  • cl/_testgop/kwargs6/: Test case for interface kwargs with Set fallback
  • cl/error_msg_test.go: Error test cases for unknown keywords and missing factory methods

Test plan

  • TestTestgop/kwargs5 — basic interface kwargs with typed methods
  • TestTestgop/kwargs6 — interface kwargs with Set fallback for unknown keys
  • TestErrInterfaceKwargsNoSet — error for unknown keyword without Set method
  • TestErrInterfaceKwargsNoFactory — error when factory method missing on receiver
  • Full cl/ test suite passes
  • Parser and printer tests unaffected

Add support for interface-based keyword arguments that desugar into
builder-pattern method chains. When a function's last parameter is a
named interface type and the receiver has a factory method matching
the interface name, keyword arguments are synthesized as chained
method calls.

For example: c.Complete ctx, maxOutputTokens = 1024, system = "hello"
desugars to: c.Complete(ctx, c.CompletionParams().MaxOutputTokens(1024).System("hello"))

Features:
- Case-insensitive keyword-to-method matching
- Optional Set(string, any) fallback for unknown keywords
- Compile-time error for unknown keywords when no Set method exists

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: xushiwei <396972+xushiwei@users.noreply.github.com>
@codecov

codecov Bot commented Mar 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.04478% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.09%. Comparing base (98c85f1) to head (27e701f).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
cl/expr.go 91.04% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2679      +/-   ##
==========================================
- Coverage   94.11%   94.09%   -0.03%     
==========================================
  Files          32       32              
  Lines        9969    10036      +67     
==========================================
+ Hits         9382     9443      +61     
- Misses        419      422       +3     
- Partials      168      171       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread cl/expr.go
Comment on lines +1089 to +1101
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" {
continue
}
if strings.ToLower(m.Name()) == kwLower {
return m.Name()
}
}
return ""
}

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).

Comment thread cl/expr.go
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.

Comment thread cl/expr.go
Comment on lines +1038 to +1062
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))
}
}

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.

@fennoai

fennoai Bot commented Mar 23, 2026

Copy link
Copy Markdown
Contributor Author

Clean implementation — the builder-pattern desugaring is well-structured, the Set fallback is a pragmatic design for extensibility, and the doc comment with the AST-equivalence example is excellent.

Test coverage gaps worth addressing:

  • Case-insensitive matching is not explicitly tested (kwargs5 would also pass with a capitalize-first-letter strategy)
  • msgIfaceNeedReceiver error path (standalone function, not a method call) has no test
  • No test for single keyword argument, or all-keywords-via-Set-fallback
  • No test for methods with incompatible signatures (e.g., multi-param or getter-style)

See inline comments for specific code-level findings.

@xushiwei
xushiwei merged commit 27e701f into main Mar 23, 2026
9 of 11 checks passed
@xushiwei
xushiwei deleted the xgopilot/issue-2678-1774232806 branch March 23, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Using Interface for Keyword Arguments

2 participants