openlists
CUE v0.18.0
Surface: per-file @experiment(...) attribute
Enablement
@experiment(openlists)Lifecycle
preview since v0.18.0
Description
OpenLists makes list literals open by default outside definitions, analogous to structs. A list without a trailing ellipsis is closed only within a definition or when closed with the close builtin. It also enables closed literals, #[...] and #{...}, which close a list or struct at its own level; #[1, 2] means what [1, 2] means without the experiment. An index on a definition named # is written (#)[i].
Specification changes
What this experiment changes in the language specification, section by section. Removed and added text is marked in place.
Describe open lists and closed literals
Values › StructsIn the specification
6 unchanged blocks
A struct is a set of elements called fields, each of which has a name, called a label, and value.
We say a label is defined for a struct if the struct has a field with the
corresponding label.
The value for a label f of struct a is denoted a.f.
A struct a is an instance of b, or a ⊑ b, if for any label f
defined for b, label f is also defined for a and a.f ⊑ b.f.
Note that if a is an instance of b it may have fields with labels that
are not defined for b.
The (unique) struct with no fields, written {}, has every struct as an
instance. It can be considered the type of all structs.
{a: 1} ⊑ {}
{a: 1, b: 1} ⊑ {a: 1}
{a: 1} ⊑ {a: int}
{a: 1, b: 1.0} ⊑ {a: int, b: number}
{} ⋢ {a: 1}
{a: 2} ⋢ {a: 1}
{a: 1} ⋢ {b: 1}The successful unification of structs a and b is a new struct c which
has all fields of both a and b, where
the value of a field f in c is a.f & b.f if f is defined in both a and b,
or just a.f or b.f if f is in just a or b, respectively.
Any references to a or b
in their respective field values need to be replaced with references to c.
The result of a unification is bottom (_|_) if any of its defined
fields evaluates to bottom, recursively.
A struct literal may contain multiple fields with the same label, the result of which is the unification of all those fields.
StructLit = "{" { Declaration "," } "}" .
StructLit = [ "#" ] "{" { Declaration "," } "}" .
Declaration = Field | Ellipsis | Embedding | LetClause | attribute .
Ellipsis = "..." [ Expression ] .
Embedding = Comprehension | Expr .
Field = Label ":" { Label ":" } Expr { attribute } .
Label = LabelExpr [ Alias ] .
LabelExpr = LabelName [ "?" | "!" ] | "[" Expr "]" .
LabelName = identifier | simple_string_lit | "(" Expr ")" .
attribute = "@" identifier "(" attr_tokens ")" .
attr_tokens = { attr_token |
"(" attr_tokens ")" |
"[" attr_tokens "]" |
"{" attr_tokens "}" } .
attr_token = /* any token except '(', ')', '[', ']', '{', or '}' */Expression Result
{a: int, a: 1} {a: 1}
{a: int} & {a: 1} {a: 1}
{a: >=1 & <=7} & {a: >=5 & <=9} {a: >=5 & <=7}
{a: >=1 & <=7, a: >=5 & <=9} {a: >=5 & <=7}
{a: 1} & {b: 2} {a: 1, b: 2}
{a: 1, b: int} & {b: 2} {a: 1, b: 2}
{a: 1} & {a: 2} _|_Values › Structs › Closed structsIn the specification
3 unchanged blocks
By default, structs are open to adding fields.
Instances of an open struct p may contain fields not defined in p.
This is makes it easy to add fields, but can lead to bugs:
S: {
field1: string
}
S1: S & { field2: "foo" }
// S1 is { field1: string, field2: "foo" }
A: {
field1: string
field2: string
}
A1: A & {
feild1: "foo" // "field1" was accidentally misspelled
}
// A1 is
// { field1: string, field2: string, feild1: "foo" }
// not the intended
// { field1: "foo", field2: string }A closed struct c is a struct whose instances may not declare any field
with a name that does not match the name of a field
or the pattern of a pattern constraint defined in c.
Hidden fields are excluded from this limitation.
A struct that is the result of unifying any struct with a ...
declaration is defined for all regular fields.
Closing a struct is equivalent to adding ..._|_ to it.
Syntactically, structs are closed explicitly with the close builtin or
implicitly and recursively by definitions.
Syntactically, structs are closed explicitly with the close builtin or
implicitly and recursively by definitions.
With the experimental @experiment(openlists) attribute, a struct literal
prefixed with #, as in #{field1: string}, is a closed struct literal:
it is closed at its own level, as with close, and, unlike the argument of
close, its fields keep the closedness of an enclosing definition.
A: close({
field1: string
field2: string
})
A1: A & {
feild1: string
} // _|_ feild1 not defined for A
A2: A & {
for k,v in { feild1: string } {
k: v
}
} // _|_ feild1 not defined for A
C: close({
[_]: _
})
C2: C & {
for k,v in { thisIsFine: string } {
"\(k)": v
}
}
D: close({
// Values generated by comprehensions are treated as embeddings.
for k,v in { x: string } {
"\(k)": v
}
})Values › ListsIn the specification
2 unchanged paragraphs
A list literal defines a new value of type list.
A list may be open or closed.
An open list is indicated with a ... at the end of an element list,
optionally followed by a value for the remaining elements.
The length of a closed list is the number of elements it contains. The length of an open list is the number of elements as a lower bound and an unlimited number of elements as its upper bound.
ListLit = "[" [ ElementList [ "," ] ] "]" .
ElementList = Ellipsis | Embedding { "," Embedding } [ "," Ellipsis ] .Lists that are open by default are an experimental feature,
enabled per file with the @experiment(openlists) attribute.
With it, a list literal without a trailing ... is open, like a struct,
and a list is closed where a struct would be:
within a definition, including a hidden definition,
and when closed with the close builtin.
A closed list admits only its own elements and those matched by its ellipsis.
Whether a list is open is determined by the file of the expression that
created it: a list literal by its own file,
and the result of a builtin by the file of the call.
@experiment(openlists)
y: [3]
y: [_, 4] // [3, 4]
#Pair: [int, int]
p: #Pair & [1, 2, 3] // _|_: index 2 is not allowedWith the experiment, a list literal prefixed with #, as in #[1, 2], is a
closed list literal: it is closed at its own level, as a list literal is
without the experiment, and its elements are not closed by it.
A closed list literal may not end in an ellipsis.
The # of a closed list or struct literal must immediately precede the
bracket.
In a file with the experiment, #[ always starts a closed list literal:
an index or slice on a definition named # is written (#)[i] or
(#)[i:j].
@experiment(openlists)
a: #[{name: "x"}]
a: [{name: "x", port: 80}] // the element is open
a: [{name: "x"}, {name: "y"}] // _|_: incompatible list lengthsListLit = [ "#" ] "[" [ ElementList [ "," ] ] "]" .
ElementList = Ellipsis | Embedding { "," Embedding } [ "," Ellipsis ] .7 unchanged blocks
Lists can be thought of as structs:
List: *null | {
Elem: _
Tail: List
}For closed lists, Tail is null for the last element, for open lists it is
*null | List, defaulting to the shortest variant.
For instance, the open list [ 1, 2, … ] can be represented as:
open: List & { Elem: 1, Tail: { Elem: 2 } }and the closed version of this list, [ 1, 2 ], as
closed: List & { Elem: 1, Tail: { Elem: 2, Tail: null } }Using this representation, the subsumption rule for lists can
be derived from those of structs.
Implementations are not required to implement lists as structs.
The Elem and Tail fields are not special and len will not work as
expected in these cases.