Skip to content

Commit 64f9226

Browse files
committed
designs/4293: add user-provided functions and validators proposal
Signed-off-by: Roger Peppe <rogpeppe@gmail.com> Change-Id: I79e2d4301f5fbed7e63591c666c0c6fe05377d88 Reviewed-on: https://cue.gerrithub.io/c/cue-lang/proposal/+/1232930 TryBot-Result: CUEcueckoo <cueckoo@cuelang.org> Reviewed-by: Marcel van Lohuizen <mpvl@gmail.com>
1 parent 3977af4 commit 64f9226

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
# Proposal: User-Provided Functions and Validators
2+
3+
Roger Peppe
4+
5+
Date: 2026-03-05
6+
7+
Discussion at https://github.com/cue-lang/cue/discussions/4293.
8+
9+
## Abstract
10+
11+
We propose adding a Go API for creating CUE-callable functions and
12+
validators from ordinary Go functions. This provides a simple, type-safe
13+
mechanism for extending CUE's evaluation with custom logic, without
14+
requiring the complexity of the existing built-in function infrastructure.
15+
16+
## Background
17+
18+
CUE's built-in functions (in the `pkg/` directory) are implemented using
19+
an internal framework that is tightly coupled to the evaluator. Adding
20+
new built-in functions requires modifying internal packages, running code
21+
generators, and understanding the details of the `adt` package. This
22+
makes it impractical for users who want to extend CUE with custom logic
23+
from their Go programs.
24+
25+
There is significant demand for user-defined functions. Common use cases
26+
include custom validation logic (checking values against external
27+
databases, applying domain-specific rules), data transformation
28+
(encoding, hashing, formatting), and integration with external services.
29+
Currently, the only way to achieve this is to evaluate CUE, extract
30+
values in Go, process them, and feed results back -- a workflow that is
31+
both verbose and loses the benefits of CUE's constraint-based evaluation.
32+
33+
The existing built-in function system also has some design choices that
34+
we would prefer not to carry forward into a user-facing API. In
35+
particular, some built-ins conflate functions and validators: for
36+
example, `list.UniqueItems` can be used both as a bare constraint
37+
(`list.UniqueItems`) and as a function call (`list.UniqueItems()`), with
38+
identical semantics. This ambiguity complicates the mental model. Some
39+
built-ins also support optional trailing arguments with default values,
40+
which introduces a form of variable-arity calling that adds complexity.
41+
42+
We propose a simpler model that avoids both of these issues while
43+
remaining fully general.
44+
45+
## Proposal
46+
47+
### Functions
48+
49+
We add a family of generic functions to the `cue` package, one for each
50+
supported argument count:
51+
52+
```go
53+
func NewPureFunc1[A0, R any](f func(A0) (R, error), opts ...FuncOption) Value
54+
func NewPureFunc2[A0, A1, R any](f func(A0, A1) (R, error), opts ...FuncOption) Value
55+
// ... through PureFunc10
56+
```
57+
58+
Each `NewPureFuncN` wraps a Go function of N arguments into a CUE `Value`
59+
that can be called from CUE expressions. The "Pure" prefix indicates
60+
that these functions are *referentially transparent*: calling them with
61+
the same arguments always produces the same result. This property is
62+
essential for CUE's evaluation model, which may evaluate expressions
63+
multiple times or in any order.
64+
65+
Arguments are decoded from CUE values to Go types using `Value.Decode`,
66+
and results are converted back to CUE values. This means that any Go
67+
type supported by `Decode` (including structs, slices, maps, and types
68+
implementing `json.Unmarshaler`) can be used as an argument type, and
69+
any Go type convertible to a CUE value can be used as a return type.
70+
71+
Here is an example of creating and using a simple function:
72+
73+
```go
74+
ctx := cuecontext.New()
75+
v := ctx.CompileString(`#add: _, x: #add(3, 4)`)
76+
v = v.FillPath(cue.ParsePath("#add"), cue.NewPureFunc2(func(a, b int) (int, error) {
77+
return a + b, nil
78+
}))
79+
fmt.Println(v.LookupPath(cue.ParsePath("x"))) // 7
80+
```
81+
82+
Note that the function is injected into the CUE value via `FillPath`
83+
into a definition (`#add`). This is one way of using them, although
84+
we anticipate that a more common pattern will be to use value
85+
injection (see https://github.com/cue-lang/cue/discussions/4294).
86+
87+
If the Go function returns a non-nil error, the CUE expression
88+
evaluates to bottom with that error message:
89+
90+
```go
91+
v = v.FillPath(cue.ParsePath("#f"), cue.NewPureFunc1(func(x int) (int, error) {
92+
if x < 0 {
93+
return 0, fmt.Errorf("negative value not allowed")
94+
}
95+
return x * 2, nil
96+
}))
97+
```
98+
99+
If the wrong number of arguments is passed in CUE, evaluation produces
100+
an error.
101+
102+
### Validators
103+
104+
We add a generic function for creating validators:
105+
106+
```go
107+
func NewPureValidatorFunc[T any](f func(T) error, opts ...FuncOption) Value
108+
```
109+
110+
A validator is a CUE value that constrains what it is unified with. When
111+
unified with a concrete value, the validator decodes that value as type
112+
`T` and calls `f` to validate it. If `f` returns a non-nil error, the
113+
unification fails with that error. If `f` returns nil, the value passes
114+
through unchanged.
115+
116+
```go
117+
ctx := cuecontext.New()
118+
v := ctx.CompileString(`#v: _, x: #v & "hello"`)
119+
v = v.FillPath(cue.ParsePath("#v"), cue.ValidatorFunc(func(s string) error {
120+
if len(s) < 3 {
121+
return fmt.Errorf("string too short")
122+
}
123+
return nil
124+
}))
125+
fmt.Println(v.LookupPath(cue.ParsePath("x"))) // "hello"
126+
```
127+
128+
Validators are distinct from functions. A function computes a new value
129+
from its arguments; a validator constrains an existing value. This
130+
distinction is clear in both the API and the CUE usage: functions are
131+
called with parentheses (`#f(x)`), while validators are unified with
132+
`&` (`#v & x`).
133+
134+
This separation does not reduce generality. A function can return a
135+
validator value, allowing patterns like `#minLen(3) & "hello"` where
136+
`#minLen` is a function that returns a validator. This composes
137+
naturally with CUE's existing constraint model.
138+
139+
### Options
140+
141+
Both `NewPureFuncN` and `ValidatorFunc` accept optional `FuncOption` values.
142+
Currently one option is defined:
143+
144+
```go
145+
func Name(name string) FuncOption
146+
```
147+
148+
`Name` sets the name used in error messages. Without it, error messages
149+
refer to the function anonymously.
150+
151+
### Exporting
152+
153+
User-provided functions and validators cannot currently be exported to
154+
CUE syntax. Attempting to format a value containing a function or
155+
validator produces an error message indicating this limitation. This is
156+
an area for future work.
157+
158+
### Restrictions
159+
160+
This initial proposal deliberately omits several capabilities:
161+
162+
- **Variable-arity functions.** Each `NewPureFuncN` requires exactly N
163+
arguments. There is no support for optional arguments or variadic
164+
calls.
165+
166+
- **Non-concrete arguments.** All arguments must be fully
167+
resolved CUE values. Non-concrete argument values are not currently
168+
supported, although they could be.
169+
170+
- **Side effects.** Only pure functions are supported. Functions that
171+
perform I/O or maintain state are not appropriate for use with this
172+
API, as CUE's evaluator makes no guarantees about evaluation order or
173+
frequency.
174+
175+
These restrictions keep the initial implementation simple and the
176+
semantics clear. Each could be relaxed in future proposals if there is
177+
demand.
178+
179+
## Rationale
180+
181+
### Why separate functions and validators?
182+
183+
The existing built-in system's conflation of functions and validators
184+
(`list.UniqueItems` vs `list.UniqueItems()`) is a source of confusion.
185+
Separating them in the user-facing API makes the mental model clearer:
186+
functions compute, validators constrain. Since a function can return a
187+
validator, there is no loss of expressiveness.
188+
189+
### Why fixed arity?
190+
191+
Supporting variable-arity functions would require either a
192+
`...interface{}` argument (losing type safety) or a more complex
193+
registration mechanism. Fixed arity with generic type parameters gives
194+
us compile-time type safety in Go while keeping the API surface small.
195+
The maximum of 10 arguments covers the vast majority of practical use
196+
cases.
197+
198+
### Why NewPureFunc and not just NewFunc?
199+
200+
The "Pure" prefix is intentional. CUE's evaluation model assumes that
201+
expressions are deterministic. Making purity explicit in the name serves
202+
as documentation and a reminder. It also leaves room for a hypothetical
203+
future `Func` (or similar) that might support side effects in a
204+
controlled manner, perhaps in the context of CUE scripting.
205+
206+
### Why not use the existing built-in framework?
207+
208+
The existing framework is designed for functions that ship with CUE
209+
itself. It uses code generation, internal types, and conventions that
210+
are not suitable for an external API. A separate, simpler API is more
211+
appropriate for user-provided functions.
212+
213+
## Compatibility
214+
215+
This proposal adds new public API surface to the `cue` package. It does
216+
not change existing CUE evaluation semantics or syntax. No existing code
217+
is affected.
218+
219+
The `NewPureFuncN` naming pattern reserves a range of names in the `cue`
220+
package. We do not expect this to conflict with any likely future
221+
additions.
222+
223+
## Implementation
224+
225+
The implementation adds the following to `cuelang.org/go`:
226+
227+
- `cue/func.go`: The `ValidatorFunc` function and the internal
228+
`pureFunc` generic implementation.
229+
- `cue/func_gen.go`: Generated `NewPureFunc1` through `NewPureFunc10`
230+
functions, produced by `cue/generate_func.go`.
231+
- `internal/core/adt`: Two new types, `Func` (implementing the `Expr`
232+
interface for callable functions) and `FuncValidator` (implementing
233+
the `Validator` interface). These integrate with the evaluator's
234+
existing function-call and validation machinery.
235+
236+
A proof-of-concept implementation exists. See
237+
https://cue.gerrithub.io/c/cue-lang/cue/+/1232921. See also the
238+
companion proposals on [value injection](./4294-value-injection.md)
239+
(which provides a mechanism for injecting functions into CUE packages
240+
without `FillPath`) and [tagged string
241+
literals](./language/4295-tagged-string-literals.md) (which
242+
demonstrates a use case for user-provided functions).
243+
244+
## Future work
245+
246+
Several directions could build on this foundation:
247+
248+
- **Function signature declarations.** A CUE syntax for declaring the
249+
expected signature of a function value, enabling static checking of
250+
call sites.
251+
- **Keyword arguments.** Support for named arguments, which would
252+
compose naturally with CUE's struct-based data model.
253+
- **Custom CUE binaries.** A mechanism for building a `cue` binary that
254+
includes custom function packages, similar to how Caddy allows custom
255+
modules.
256+
- **RPC protocol.** An IPC-based protocol for executing custom functions
257+
in a separate process, removing the requirement that functions be
258+
implemented in Go and enabling language-agnostic extensibility.

0 commit comments

Comments
 (0)