-
Notifications
You must be signed in to change notification settings - Fork 566
Type as Func Arguments (based on generic)
XGo extends Go's generic function call syntax by allowing type parameters that cannot be inferred to be passed as leading positional arguments, rather than requiring the explicit bracket syntax foo[T1, T2, ...]. This is opt-in per function, controlled by the XGox_ naming prefix convention.
Go's generic function call syntax requires users to supply non-inferrable type parameters inside square brackets:
foo[T1, T2, ...](arg1, arg2, ...)While precise, this syntax is unfamiliar to beginners, especially those coming from dynamically typed or scripting backgrounds. XGo aims to lower the barrier for new programmers and make generic APIs feel more natural and expressive.
A Go function author opts into this feature by prefixing the function name with XGox_:
func XGox_foo[T1 I1, T2 I2, ..., Tm Im, Tm+1 Im+1, ..., Tn In](arg1 ArgT1, arg2 ArgT2, ...)Here:
-
T1, T2, ..., Tmare type parameters that cannot be inferred from the call-site arguments and must be supplied explicitly. -
Tm+1, ..., Tnare type parameters that can be inferred and need not be supplied by the caller.
XGo exposes XGox_foo to callers as simply foo. The non-inferrable type parameters T1, ..., Tm are passed as leading positional arguments:
Expression form:
foo(T1, T2, ..., Tm, arg1, arg2, ...)Command form (statement context):
foo T1, T2, ..., Tm, arg1, arg2, ...The compiler recognises that the first m arguments are types (not values) and rewrites the call to:
XGox_foo[T1, T2, ..., Tm](arg1, arg2, ...)// Package conv provides type-safe conversion utilities.
// XGox_As converts src to type T.
// T cannot be inferred because it only appears in the return position.
func XGox_As[T any](src any) (T, error) {
v, ok := src.(T)
if !ok {
return *new(T), fmt.Errorf("cannot convert %T to %T", src, *new(T))
}
return v, nil
}import "conv"
// Expression form
v, err := conv.As(int, rawValue)
// Command form (in a statement context)
conv.As int, rawValueBoth are equivalent to the Go call conv.XGox_As[int](rawValue).
Applying this transformation to every generic function would be too broad and could introduce ambiguities (e.g. a reflect.Type value passed as a regular argument). Requiring the XGox_ prefix ensures that only explicitly authored functions participate, keeping the feature's footprint small and the compiler's job unambiguous.
Leading placement mirrors natural language reading order — "give me an int version of this value" — and avoids confusion with optional trailing variadic arguments.
Only the m non-inferrable type parameters must be provided; the remaining Tm+1, ..., Tn are still inferred as usual. The caller never needs to think about inferrable parameters.
XGox_foo is exposed in XGo as foo. The prefix is a compiler-level convention, invisible to XGo callers. IDEs and documentation tools should strip it when presenting the API to XGo users.
The XGo compiler distinguishes type arguments from value arguments at the call site using the same type-vs-expression analysis it already performs for other constructs. A leading argument that resolves to a type identifier (or a composite type expression such as []int, map[string]int) is treated as a type argument; anything else is treated as a value argument.
This means the following are all valid:
foo([]byte, data) // T = []byte
foo(map[string]int, data) // T = map[string]int
foo(*MyStruct, data) // T = *MyStructThis feature lives entirely in XGo's compiler front-end. The generated Go code uses standard generic instantiation syntax (XGox_foo[T](...)), so:
- No changes to the Go compiler or runtime are required.
- Go consumers of an
XGox_-prefixed function continue to use the normal bracket syntax. - Interoperability and binary compatibility are unaffected.
| Layer | Name seen | Meaning |
|---|---|---|
| Go source | XGox_foo |
Opt-in marker; type params are leading when called from XGo |
| XGo source | foo |
Clean name; prefix is stripped by the compiler |
| Generated Go | XGox_foo[T1, …, Tm](…) |
Standard generic instantiation |
XGo also supports a separate feature where a function accepts reflect.Type values as its arguments, allowing callers to pass a bare type expression in its place. These two features must not be combined in a single call.
Specifically, if an XGox_-prefixed function has reflect.Type among its value parameters, the caller must not pass a bare type expression for that reflect.Type argument. Instead, the caller must use reflect.TypeFor[T]() explicitly.
Consider a function with one non-inferrable generic type parameter T and a reflect.Type value parameter:
// Declaration (in Go)
func XGox_foo[T any](t reflect.Type, ...) { ... }The XGo call requires the generic type argument T as a leading bare type, while the reflect.Type parameter must be supplied with an explicit reflect.TypeFor[...]():
// WRONG — bare type for the reflect.Type argument is not allowed
foo(int, string, someValue)
// ^^^ ^^^^^^
// | |
// | ambiguous: is this T or a reflect.Type shorthand?
// T (generic, OK as bare type)
// CORRECT — T is passed as a bare type; reflect.Type is passed explicitly
foo(int, reflect.TypeFor[string](), someValue)
// expands to: XGox_foo[int](reflect.TypeFor[string](), someValue)This rule prevents ambiguity: within an XGox_-prefixed call, leading bare type expressions are always and only consumed as generic type arguments T1, ..., Tm. A reflect.Type value parameter is never filled by a bare type expression, regardless of its position in the signature.
If a package exports both foo and XGox_foo, the XGo compiler raises a compile-time error rather than silently picking one:
ambiguous name "foo": package exports both "foo" and "XGox_foo"
Package authors are responsible for ensuring no such conflict exists. This makes the naming contract explicit and avoids surprising shadowing behaviour.
Go Is XGo's "Assembly Language"
That framing sounds bold, but it is precise. XGo does not treat Go as a mere transport medium the way some "compiles-to-Go" languages do. XGo treats Go as its semantic foundation:
- Every XGo package can be translated one-to-one into a Go package.
- The translated Go package preserves semantics exactly and can be consumed by any standard Go toolchain.
- Conversely, that translated Go package can also be imported by other XGo code, with no bridging layer required.
This means XGo's type system, memory model, and concurrency primitives are all inherited directly from Go. Go's compiler-level verification applies uniformly to all XGo source — no exceptions, no carve-outs.
This proposal is a direct example of that pattern. The XGox_-prefixed function must be defined in Go, not in XGo. XGo currently has no grammar to express a generic function declaration with the XGox_ prefix semantics — the prefix convention is a contract between the Go author and the XGo compiler, not something XGo source can produce itself.
This is intentional and consistent with XGo's broader design philosophy: when a feature requires low-level or structurally complex declaration syntax that XGo does not yet surface, Go serves as the "assembly language" in which that declaration is written. The XGo layer then provides the ergonomic call-site syntax that makes the feature accessible to end users.
This pattern appears across XGo's ecosystem wherever the expressive gap between Go and XGo is temporarily bridged by Go-authored glue — until XGo grows the grammar to handle it natively.
-
Method support —
XGox_will also apply to methods, but the details are more involved and will be addressed in a dedicated follow-up proposal. -
Tooling —
gopls,xgo doc, and other tools should be updated to strip the prefix and present the clean signature to XGo users.