functions

CUE v0.18.0

Surface: per-file @experiment(...) attribute

Enablement

@experiment(functions)

Lifecycle

preview since v0.18.0

Description

Functions enables experimental function signatures and native CUE function bodies.

Specification changes

What this experiment changes in the language specification, section by section. Removed and added text is marked in place.

Specify experimental functions

proposed not yet in the specification · 2026-09-16 · v0.18.0

Lexical elements › Keywords › Expressionsnew section

The following keyword introduces a function literal.

func

Lexical elements › Operators and punctuationIn the specification

The following character sequences represent operators and punctuation:

+     &&    ==    <     =     (     )
-     ||    !=    >     :     {     }     ~
-     ||    !=    >     :     {     }     ~     ->
*     &     =~    <=    ?     [     ]     ,
/     |     !~    >=    !     _|_   ...   .

Expressions › OperandsIn the specification

Operands denote the elementary values in an expression. An operand may be a literal, a (possibly qualified) identifier denoting a field, alias, or let declaration, or a parenthesized expression.

Operand     = Literal | OperandName | "(" Expression ")" .
Literal     = BasicLit | ListLit | StructLit .
Literal     = BasicLit | ListLit | StructLit | FuncLit .
BasicLit    = int_lit | float_lit | string_lit |
              null_lit | bool_lit | bottom_lit .
OperandName = identifier | QualifiedIdent .

Expressions › Primary expressionsIn the specification

Primary expressions are the operands for unary and binary expressions.

PrimaryExpr =
	Operand |
	PrimaryExpr Selector |
	PrimaryExpr Index |
	PrimaryExpr Arguments |
	PrimaryExpr "..." .

Selector       = "." (identifier | simple_string_lit) .
Index          = "[" Expression [ "," ] "]" .
Argument       = Expression .
Argument       = [ identifier ":" ] Expression .
Arguments      = "(" [ ( Argument { "," Argument } ) [ "," ] ] ")" .
cue
x
2
(s + ".txt")
f(3.1415, true)
m["foo"]
obj.color
f.p[i].x

Expressions › Function literalsnew section

Function literals are an experimental feature, enabled per file with the @experiment(functions) attribute.

A function literal denotes a function value: an opaque value that computes a result from a set of arguments when called. A function value unifies with other function values with a compatible signature, which composes them, and with function types; unification with any other value results in bottom (_|_). A partially applied function unifies only with itself, the result of the same call.

FuncLit    = ClosedFunc | OpenFunc .
ClosedFunc = "func" "(" [ ParamList [ "," ] ] ")" [ "->" Expression ] [ ":" Expression ] .
OpenFunc   = "func" "(" [ ParamList "," ] "..." [ "," ] ")" [ "->" Expression ] .
ParamList  = ParamDecl { "," ParamDecl } .
ParamDecl  = [ identifier [ "~" identifier ] [ "!" | "?" ] ":" ] Expression [ "=" Expression ] { attribute } .

A parameter declaration is a restricted form of declaration: let clauses, attribute declarations, and comprehensions are not permitted, and a parameter’s label must be a single, non-definition identifier: the shorthand notation for nested structs (a: b: T) is not permitted, and neither are string, pattern, dynamic, and definition labels. A bare expression declares an anonymous parameter, as does the blank identifier _, which may not be marked optional or required, as such a parameter could never be supplied. An anonymous parameter has no name at all, whereas a positional-only parameter (_~x) has a name for use in the body but none that a caller can use. An ellipsis must be the final element of the list and marks a bodyless signature as open. An open signature cannot have a body. Attributes attached to a parameter are treated like field attributes; they do not influence evaluation.

The parameters name the values that a call supplies and may constrain them. The first expression of a parameter is its constraint. The expression after =, if present, is the parameter’s default: the argument used when a call omits the parameter. An optional parameter cannot have a default, since it is absent when omitted; a default on a required parameter makes it omittable, as on a named parameter. A default mark (*) written directly in a parameter’s constraint or default is an error: a parameter default is declared with =, and a mark reached through a reference or nested inside a literal is an ordinary part of the value. The expression following -> constrains the result of a call. The expression following the final : is the body, which defines the result of a call. A literal with a body is a closed function value. A literal without a body denotes a function type, which may be open or closed.

Parameters take the following forms:

| Declaration     | Kind            | Bound by                              | Name in the body |
|-----------------|-----------------|---------------------------------------|------------------|
| `a: int`        | named           | position, or the label `a`            | `a`              |
| `a~x: int`      | named           | position, or the label `a`            | `x`              |
| `a!: int`       | required        | the label `a` only; must be supplied  | `a`              |
| `a?: int`       | optional        | the label `a` only; may be omitted    | `a`              |
| `_~x: int`      | positional-only | position only                         | `x`              |
| `int`, `_: int` | anonymous       | position only                         | none             |

A local name (~x) may be given to any named form, including a required or optional one; callers use the label, and the body uses the local name. A default does not change how a parameter is bound: a: int = 2 can be bound by position or by the label a, and 2 is the argument when a call omits it.

Named, positional-only, and anonymous parameters can be bound by position and are called positional parameters; required and optional parameters are bound by label only. A parameter that cannot be bound by label — anonymous or positional-only — may not follow a parameter that is declared with a name. A positional parameter may not follow a required or optional parameter. Parameter names must be unique within a parameter list. The dual form of a postfix alias may not be used in a parameter.

Parameter constraints, parameter defaults, and the result constraint are resolved in the scope in which the literal is declared, not in the body’s scope, and may refer to a field of an enclosing scope. Referring to another parameter is reserved for a possible future extension and is an error. Within the body, parameters shadow fields of enclosing scopes. A function value captures the scope in which its literal is declared.

A parameter default belongs to the signature, not to the values a call passes: a caller cannot pass, store, or refer to a parameter’s default, and a parameter receives the default’s value only when a call omits it. Each default is checked against its parameter’s constraint once the value declaring the function literal has been evaluated, and a conflict is an error whether or not the function is ever called.

@experiment(functions)
 
base:  10
sum:   func(a: int, b: int) -> int: a + b
add1:  func(_~x: int) -> int: x + 1
pick:  func(a: int = 5) -> int: a
key:   func(a!: int, b?: int) -> int: a
opt:   func(a!: int = 1) -> int: a // name-only, with a default
scale: func(a: int) -> int: a * base // captures base
 
limit: int | *7
f:     func(a: string, b: limit) -> int: b // b's constraint refers to limit
out:   f("shadow", int) // int | *7
 
bad:   func(a: int = "x") -> int: 1 // _|_ // invalid default for parameter a

Referring to a parameter from another parameter’s constraint or from the result constraint is an error:

@experiment(functions)
 
f: func(x: int, y: >x) -> int: x

Parameters that can only be bound positionally must precede named parameters:

@experiment(functions)
 
bad: func(a: int, int) -> int: 1

A positional parameter may not follow a required or optional parameter:

@experiment(functions)
 
bad: func(a?: int, b: int) -> int: b

Expressions › Function typesnew section

A function literal without a body denotes a function type. Calling a function type is an error. A trailing ellipsis marks a signature as open: an open type is a partial signature that admits parameters beyond the ones it declares. Without an ellipsis a signature is closed. An open type becomes callable only by unification with a closed function value that supplies its complete parameter slots and implementation. Unifying it with a closed bodyless signature still produces a function type and does not make it callable.

Unifying two function types matches their parameters. A positional parameter matches the positional parameter at the same ordinal. For each matched pair, an absent label is compatible with a present one, and the resulting parameter acquires that label. If both parameters have labels, the labels must be identical; different labels for the same ordinal are incompatible. The same label may not identify different ordinals. A required or optional parameter matches the parameter of the other type that has the same label, whether that parameter is positional or not. The result retains both signatures; the constraints of matched parameters and the result constraints apply jointly. The markers of matched parameters combine as those of field constraints do: a call must supply the parameter unless all of them mark it optional, and a named parameter keeps its position. For instance, (func(a: int) -> int) & (func(a?: int) -> int) has a named parameter a, and (func(a!: int) -> int) & (func(a?: int) -> int) a required one. A parameter of one type that matches no parameter of the other is an error unless the other type is open or the parameter is optional or declares a default. The unified type is open only if both types are open.

Defaults are part of a signature. A function type may declare parameter defaults; a default declared by one of the unified signatures applies to calls through the result, and defaults declared by both for the same parameter must unify: a disagreement is an error when the signatures are unified. Neither signature overrides the other.

@experiment(functions)
 
T:  func(a: int = 2) -> int
f:  T & (func(a: int) -> int: a)
x:  f()  // 2, the type's default applies
e:  T & (func(a: int = 3) -> int: a) // _|_ // conflicting defaults for parameter a

The label of a named parameter is part of the callable contract in addition to its position. When a function type is unified with a callable value, the label of a type’s positional parameter names the matched positional parameter of the value if that parameter has no label of its own. Another function type may repeat that label, but it may not give the same positional parameter a different label.

Unifying a function type with a function value tightens the value: the result is the function value with the type’s parameter constraints and result constraint enforced on every call. Every parameter of the type must match a parameter of the value, unless it is optional or declares a default. Unless the type is open, every parameter of the value must match a parameter of the type, unless it is optional or declares a default itself: a call through the type never needs to supply it. A required or optional parameter of the type also matches a positional parameter of the value that has the same label, but a positional parameter of the type must match a positional parameter of the value, as a call through the type may pass it by position. The markers of matched parameters combine as for two types. Parameter constraints follow the same matching as call labels. Whether a constraint of the type is compatible with the value is not decided at unification time: constraints are enforced when the tightened value is called, in the scope in which the type was declared.

Unifying two function values composes them: a call of the result binds its arguments to the parameters of both, evaluates both bodies, and unifies their results. Their signatures unify as for two function types, except that a positional parameter of one must match a positional parameter of the other, and a call can bind only the parameters both declare; a parameter only one of them declares must be omittable and is omitted.

@experiment(functions)
 
f: func(a: int) -> _: {in: a}
g: func(a~x: int) -> _: {out: x + 1}
h: f & g
r: h(1)                                          // {in: 1, out: 2}
c: (func(a: int) -> int: a) & (func(a: int) -> int: a + 1)
e: c(1)                                          // _|_ // conflicting values 1 and 2
b: f & (func(b: int) -> _: {}) // _|_ // conflicting parameter labels

The predeclared functions and the functions of the standard library are function values whose signatures are declared in CUE and whose bodies are provided by the implementation, and are unified and called like any other function value. At this experimental stage, some of them do not yet declare a signature, they do not yet compose with other function values, and a standard-library function that can also construct a validator treats a call with fewer arguments than parameters as such a construction.

@experiment(functions)
 
import (
	"path"
	"strings"
)
 
a: strings.Repeat("ab", count: 2) // "abab"
 
// A compatible anonymous type constrains a parameter
// of strings.ToUpper without replacing its label.
b: (func(string) -> string) & strings.ToUpper
c: b("hi")     // "HI"
d: b(s: "bye") // "BYE"
 
// A different label for the same parameter is incompatible.
e: (func(text: string) -> string) & strings.ToUpper // _|_
 
// A default declared by a type fills an omitted argument.
_r: strings.Repeat
_r: func(s: string, count: int = 2) -> string
r:  _r("ab") // "abab"
 
// The os parameter of path.Join declares the default "unix".
j: path.Join(["a", "b"]) // "a/b"
w: (func(elem: [...string], os: string = "windows") -> string) & path.Join // _|_ // conflicting defaults for parameter os
@experiment(functions)
 
T: func(a: int, ...) -> number // open: admits further parameters
U: func(a: int) -> number      // closed
 
f: T & (func(a: int, b: int) -> int: a + b)
x: f(1, 2) // 3
y: f(1)    // _|_ // missing argument b
 
g:  U & (func(a: int) -> int: a)          // exact match
g1: g(1)                                  // 1
h:  U & (func(a: int, b?: int) -> int: a) // extra parameter is optional
hd: U & (func(a: int, b: int = 2) -> int: a + b) // extra parameter has a default
h1: hd(1)                                 // 3
fromUnnamed: U & (func(_~v: int) -> int: v) // unnamed slot acquires label a
r1: fromUnnamed(a: 1)                       // 1
r2: fromUnnamed(2)                          // 2
e1: U & (func(b: int) -> int: b)            // _|_ // labels a and b conflict
e2: U & (func(a: int, b: int) -> int: a) // _|_ // b not admitted by closed U
 
// Constraints are enforced per call, in the type's scope.
s:  T & (func(a: string) -> int: 1)
e3: s(1) // _|_ // conflicting values int and string
 
Narrow: func(input: <10) -> int
narrowed: Narrow & (func(_~x: int) -> int: x)
e4: narrowed(input: 15) // _|_ // input's constraint follows slot 0

Expressions › CallsIn the specification

Calls can be made to core library functions, called builtins. Given an expression f of function type F,

Given an expression f whose value can be called,

f(a1, a2, … an)

calls f with arguments a1, a2, … an. Builtin functions can be called; with the functions experiment, so can function values. Arguments must be expressions of which the values are an instance of the corresponding parameter types of Fconstraints and are evaluated before the function is called.

2 unchanged blocks
cue
a: math.Atan2(x, y)

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.

With the functions experiment, an argument is positional or labeled; positional arguments must precede labeled arguments. Positional arguments bind the function’s positional parameters in order; a labeled argument binds the parameter with that label. The label may be declared by the callable value itself or given to an otherwise unnamed positional parameter by a function type unified with it. Arguments are evaluated in the scope of the caller.

A call results in bottom (_|_) if it binds a parameter both by position and by label, labels a parameter that is unknown or not bindable by label, supplies more positional arguments than there are positional parameters, or omits a parameter that is neither optional nor declares a default. Whether a parameter may be omitted is decided by its declaration alone, never by what its constraint evaluates to.

When a call omits a parameter that declares a default, the default is the argument. An omitted optional parameter has no value: referring to it is an error, as referring to an absent optional field is, so a body guards such a reference:

@experiment(functions)
 
offset: func(a: int, by?: int) -> int: [if by != _|_ {a + by}, a][0]
o1:     offset(1)        // 1
o2:     offset(1, by: 2) // 3

A supplied argument, by position, by label, or by an earlier partial application, replaces the default entirely: an argument that carries its own default keeps it, and a non-concrete argument does not cause the default to apply.

The result of a call is the unification of the body and the result constraint, evaluated with each parameter set to the unification of its constraint and its argument.

@experiment(functions)
 
sum:  func(a: int, b: int) -> int: a + b
key:  func(a!: int, b?: int) -> int: a
pick: func(a: int = 5) -> int: a
add:  func(a: int, b: int = 10) -> int: a + b
x:    int | *5
 
p: sum(1, 2)       // 3
l: sum(a: 1, b: 2) // 3
m: sum(1, b: 2)    // 3
k: key(a: 4)       // 4
d: pick()          // 5
o: add(1)          // 11
s: add(1, x)       // 6: x's own default applies, not b's
 
e1: sum(1, 2, 3)            // _|_ // too many positional arguments
e2: sum(a: 1, b: 2, c: 3)   // _|_ // unknown argument c
e3: sum(1, a: 2)            // _|_ // argument a provided by position and label
e4: key(4)                  // _|_ // missing required argument a
e5: sum(1)                  // _|_ // missing argument b
e6: (func() -> string: 1)() // _|_ // conflicting values 1 and string

A call whose argument list ends in ... is a partial application: it binds the given arguments and yields a function over the remaining parameters instead of evaluating the body. The supplied arguments are retained; a later call combines them with its own arguments, and the body is evaluated once every parameter is supplied or omitted. A supplied parameter loses its default; the remaining parameters keep theirs, which apply when the completing call omits them. Partial applications may be chained.

At this experimental stage, a function type must be unified with a function value before the value is partially applied: unifying a function type with an already partially applied value is an error. A function type unified first remains in force, and its labels and constraints carry across to the parameters that remain.

@experiment(functions)
 
add: func(a: int, b: int, c: int) -> int: a + b + c
 
f: add(1, ...)    // a function of b and c
g: f(2, ...)      // a function of c
x: g(3)           // 6
y: add(a: 1, ...)(2, 3) // 6

Recursion, direct or mutual, does not terminate in finite structure and results in a structural cycle error. Calls do not otherwise constitute cycles: calls may be nested and repeated, including nested calls to the same function.

@experiment(functions)
 
fib: func(n: int) -> int: fib(n-1) + fib(n-2)
f:   fib(5) // _|_ // structural cycle
 
twice: func(n: int) -> int: n + n
t:     twice(twice(twice(2))) // 16

A positional argument may not follow a labeled argument:

@experiment(functions)
 
sum: func(a: int, b: int) -> int: a + b
out: sum(a: 1, 2)
Install CUE