For the complete documentation index, see llms.txt
Compact reference
Overview
Compact is a strongly typed, statically typed, bounded smart contract language, designed to be used in combination with TypeScript for writing smart contracts for the three-part structure of Midnight, where contracts have the following components:
- a replicated component on a public ledger,
- a zero-knowledge circuit component that confidentially proofs the replicated component resulted from a valid execution according to the logic of the contract, and
- a local, off-chain component that can perform arbitrary code.
Each Compact program (also referred to as a contract) can contain several kinds of program elements:
- module and import forms for management of namespaces and separate files,
- declarations of program-defined types,
- declarations of the data that the contract stores in the public ledger,
- declarations of witnesses, which are callback functions supplied by the TypeScript runner,
- definitions of circuits, which are functions serving as the operational core of a smart contract, and
- the definition of at most one constructor, which is a function called when a contract is created and can be used to initialize public and private state.
Compact is similar to TypeScript: it has a syntax similar to that of JavaScript, and it layers a type system over the JavaScript syntax. Compact deviates intentionally from TypeScript, however, in several important ways:
- Unlike TypeScript, Compact is strongly typed. Programs cannot bypass the static type system via missing type declarations or unsafe casts. Furthermore, the JavaScript code produced by the Compact compiler includes run-time checks enforcing the static types of values that come from outside Compact as well as preventing external application of a Compact circuit with more or fewer than the declared number of arguments.
- Compact provides namespace management via static rather than dynamic modules, and these modules can be parameterized via compile-time generic parameters, which include size as well as type parameters.
- Because every Compact program must compile into a set of finite proving circuits, the language is bounded: all Compact types have sizes that are fixed at compile time, loops are bounded either by constant bounds or by the size of an object of constant size, and recursion is disallowed.
- Compact numeric values are limited to unsigned integers either with a program-declared range or with a range determined by the field size of the target proving system.
- Compact distinguishes certain values as potentially containing private data that
should typically be protected, and it requires explicit declaration of the
disclosure of any potential private data via
disclose()wrappers. The basics of this are discussed in the description ofdisclose, and a more thorough description and discussion appears in the separate document Explicit disclosure in Compact.
Like TypeScript, Compact compiles into JavaScript, but it also produces a TypeScript definition file so it effectively also compiles into TypeScript. It produces separate TypeScript definition files and JavaScript implementation files rather than simply producing TypeScript for three reasons:
- to allow compiled Compact programs to be used without requiring an additional TypeScript compilation step,
- to permit the generated code to check function argument counts without disabling compile-time argument-type checks when called from TypeScript, and
- so that it can generate a source-map file that properly maps elements of the generated JavaScript code (e.g., variable bindings and references) to the corresponding elements of the source Compact code.
For each circuit that touches the public ledger and hence requires a proof for on-chain execution, the Compact compiler also produces proving circuits in a zero-knowledge intermediate language (zkir), and it uses a zkir compiler to produce proving keys for each such circuit.
Finally, the compact compiler also produces a JSON-formatted contract info file that contains information about the program and its compiled representation, including version numbers and the types and characteristics of the contract's exported circuits.
This document explains each syntactic category individually. It starts by introducing the building blocks that are used in various contexts: identifiers, constants, types, generic parameters, and patterns. Then it describes the structure of Compact programs, each kind of program element, and the statements and expressions that can appear within circuit and constructor bodies. Finally, it discusses the TypeScript target.
Writing a contract provides a small example of what a Compact program looks like. It also introduces the basic building blocks of a Compact contract. The full grammar of Compact is provided separately.
Notation
The syntax of Compact programs is given by EBNF grammar snippets that use the following notational conventions:
- Keywords and punctuation are in
monospacedfont. - Terminal and nonterminal names are in emphasized font.
- Alternation is indicated by a vertical bar (
|). - Optional items are indicated by the superscript opt.
- Repetition is specified by ellipses.
The notation X ⋯ X, where X is a grammar symbol, represents zero
or more occurrences of X.
The notation X
,⋯,X, where X is a grammar symbol and,is a literal comma, represents zero or more occurrences of X separated by commas. In either case, when the ellipsis is marked with the superscript 1, i.e., ⋯¹, the notation represents a sequence containing at least one X. When such a sequence is followed by,opt, an optional trailing comma is allowed, but only if there is at least one X. For example, id ⋯ id represents zero or more ids, and expr,⋯¹,expr,opt represents one or more comma-separated exprs possibly followed by an extra comma. The rules involving commas apply equally to semicolons, i.e., apply when,is replaced by;.
Every program is formed of characters that are organized into atomic
sequences of characters known as tokens.
Each keyword and punctuation symbol appearing in the grammar snippets
represents itself exactly, i.e., represents the token consisting of
the same sequence of characters.
For example, when the keyword circuit appears in a grammar snippet,
it matches only the token circuit, and when the punctuation symbol
: appears, it matches only the token :.
Each terminal name appearing in the grammar snippets represents a set of possible tokens. For example, the terminal name id represents the set of all identifiers: when id appears in a grammar snippet, it matches any identifier. The sets of tokens represented by the terminal names appearing in the grammar snippets are described in Terminal names below.
Each nonterminal name appearing in the grammar snippets represents a
sequence of tokens that comprise some structured piece of a program.
For example, the terminal name expr matches any sequence of tokens
that can be interpreted as an expression, such as 3 + x or
a ? b : c.
The set of structures represented by each nonterminal name is given
in the various sections of this reference manual along with typing
and evaluation rules where appropriate.
For example, the structure of a circuit-definition is described
in Circuits definitions.
Terminal names
The following terminal names appear in the grammar snippets.
-
id, module-name, function-name, struct-name, enum-name, contract-name, tvar-name, and type-name all represent identifier tokens.
-
nat represents natural-number literals.
-
str and file represent string literals.
-
version represents version literals (in pragmas).
While identifiers and string literals are each represented by more than one name, each represents the entire set of possible identifier or string-literal tokens. The grammar snippets use different terminal names only to suggest their use, e.g, module-name for module names versus tvar-name for type variable names.
Static and dynamic errors
The compiler detects various kinds of static errors, e.g., malformed syntax, references to undefined identifiers, and type mismatches. When it detects one or more static errors, it prints descriptive error messages for the errors and terminates without generating any output.
The code the compiler generates and the run-time libraries it uses detect various kinds of dynamic errors, e.g., attempts from code outside of Compact to call Compact circuits with wrong numbers or types of arguments or attempts to cast an unsigned value to an unsigned type that is too small for the value. These errors are reported when the generated code is run and cause evaluation of the current circuit to halt.
Identifiers, bindings, and scope
Identifiers are used in Compact, as in most other programming languages, to
name things.
Syntactically, an identifier is a token (atomic sequence of characters), beginning
with with an alphabetic character, a dollar sign ($), or an underscore (_)
followed by one or more alphabetic characters, digits (0 - 9), dollar signs,
or underscores.
Some identifiers are reserved words.
Of these, some are used as keywords in the syntax of the Compact language, e.g.,
module, import, circuit, and for.
Others, specifically keywords reserved by JavaScript and TypeScript, are considered
reserved for future use in Compact, e.g., self and class.
Still others, specifically every identifier that begins with __compact,
are reserved for use by the compiler.
A comprehensive list of keywords and reserved words is given
in keywords and reserved words.
The remaining identifiers can be used to name specific instances of various kinds of entities, including modules, types, generic parameters, ledger fields, function (circuit or witness) names, function parameters, and local variables. An identifier associated with, i.e., bound to, one of these entities can be referenced anywhere within the scope of the binding. Compact is lexically scoped, so the scope of each binding is limited to a specific region of the program text. The binding might be shadowed (hidden from view) in some region of program text within its scope that contains a binding for the same identifier.
It is a static error for an identifier to be bound more than once in a same scope, except that function overloading allows more than one function with the same name to be visible in the same scope with different signatures, i.e., different numbers or kinds of generic parameters and/or different numbers or types of run-time parameters.
The scope of each binding depends upon where it appears, as described below. (The caveat "except where shadowed" is not explicitly stated but applies in each case.)
- Identifiers bound at the outermost level of a contract (referred to as the top level) are visible throughout the contract, but not within any modules that are imported from separate files.
- Identifiers bound at the outermost level of a module are visible throughout the module. They are not visible outside of the module unless exported: any exported binding is also visible if and where it is imported from the module.
- The generic parameters of a module, structure declaration, or function declaration are visible throughout the declaration.
- The run-time parameters of a circuit or constructor are visible within its body.
- Identifiers defined by a
constbinding inside a block are visible throughout the block. - Identifiers defined by a
constbinding inside afor-loop header are visible throughout theforloop.
Every reference to an identifier must appear within the scope of a binding for the identifier, in which case we say that the identifier is bound to the entity associated with the identifier by that binding. Otherwise, the reference is a static error.
For example:
circuit c(): Field {
const answer = 42;
{
const answer = 12;
assert(answer != 42, "shadowing did not work!");
}
return answer; // returns 42 (the outer 'answer')
}
The identifier c is bound to the circuit named c, and this binding is visible
throughout the contract, though no references to c appear in the example.
The first (outer) binding of the identifier answer to the value 42 is visible
throughout the body of c except where shadowed by the second (inner) binding of
answer within in the inner block, so the reference to answer in return answer
evaluates to 42.
The second (inner) binding of answer to 42 is visible throughout the inner
block, so the reference to answer in answer != 42 evaluates to 12.
In addition to having a scope, every binding also has a lifetime. For circuit and witness bindings, the lifetime is effectively permanent, i.e., the binding is always available for use whenever the program is run.
The lifetimes of ledger-field bindings begin when they are first initialized and are effectively permanent from that point on; although the value of a field can change over time, the association of the ledger-field name with the ledger field's location in the (replicated) public state of a contract never changes.
On the other hand, bindings for module names, type names, and generic parameters are live only when a program is compiled, i.e., they help determine the structure of the program and the shape of the data used by the program but are not needed once the program has been compiled. (TypeScript bindings for type names exported from the program's top level do live on, however, in the generated TypeScript definition file.)
Variable bindings, i.e., bindings of circuit parameters, constructor parameters,
and local variables bound by const statements and for loops, have dynamic
lifetimes.
The bindings of a circuit's or constructor's parameters start a new lifetime
when the circuit or constructor is called that ends when the circuit or constructor
exits.
A variable binding established by a const statement starts a new lifetime when
the const statement is evaluated that ends when the block containing the const
statement exits.
A const binding established by the const subform of a for-loop header starts
a new lifetime on each iteration of the loop that ends when that iteration ends.
Variable bindings can have multiple lifetimes, because a circuit might be called
multiple times, a block might be evaluated multiple times, and a for loop might
be evaluated multiple times and/or have multiple iterations.
Variables in Compact are immutable, however: they have the same value over the
entire lifetime of the variable's binding.
Thus, they are referred to as variables not because their values can vary over
any single lifetime but because they can have different values in different
lifetimes.
Generic parameters and arguments
Various entities, specifically module declarations, structure declarations, type-alias declarations, circuit definitions, and witness declarations, can have generic parameters, i.e., compile-time type and numeric (natural-number) parameters whose values are given at the use site rather than fixed at the point of declaration. This allows the same generic code to be used with different specific types, bounds, and sizes. Except where shadowed, generic parameters are visible throughout the entire entity. In particular, the generic parameters of a module are visible within the program elements that appear within the body of the module.
When present, generic parameters are enclosed in angle brackets following the
name of the generic entity (module, structure, type alias, circuit, or witness).
Each parameter is either a type name (e.g., T) or a hash-prefixed natural-number name
(e.g., #N).
Generic natural-number parameters are prefixed by # to distinguish them from
generic type parameters.
| gparams | ⟶ | < generic-param , ⋯ , generic-param ,opt > |
| generic-param | ⟶ | # tvar-name |
| | | tvar-name |
Generic entities must be specialized at the point of use to produce non-generic entities at compile time by supplying them with generic arguments. Any attempt to use a generic entity without specializing it is a static error. Generic arguments are also enclosed in angle brackets. Each generic argument must be a type, a natural number literal, or the type or numeric value of a generic parameter.
The syntax of types allows for type references, including references to generic parameters, so any generic argument can pass along the value of a generic type or natural-number parameter that is visible at the point of specialization.
The # used to distinguish generic natural-number parameters from generic type parameters
need not and must not be used at the point of specialization.
It is a static error, however, if a generic argument supplied for a generic
parameter is not numeric when a numeric value is expected or is not a type when
a type is expected.
The example below demonstrates the use of two levels of generic parameterization, one at the module level and one at the circuit level.
module M<T, #N> {
export circuit foo<A>(x: T, v: Vector<N, A>): Vector<N, [A, T]> {
return map((y) => [y, x], v);
}
}
import M<Boolean, 3>;
export circuit bar1(): Vector<3, [Uint<8>, Boolean]> {
return foo<Uint<8>>(true, [101, 103, 107]);
}
export circuit bar2(): Vector<3, [Boolean, Boolean]> {
return foo<Boolean>(false, [true, false, true]);
}
The body of circuit foo is generic with respect to the module's type parameters
T and N as well as to the circuit's own parameter A.
The module is specialized at the point of import, while the circuit is specialized
at the point of call (in both bar1 and bar2).
Compact types
Compact is statically typed: every expression in a Compact program must have
a static type.
For named circuits and witnesses, the parameters and return types must be explicitly
declared.
For anonymous circuit expressions, the parameters and return types do not need to
be declared but can be.
The types of const bindings can also be declared or not.
The language is strongly typed: the compiler rejects programs that do not type check. For example, it rejects programs in which a circuit or witness with a parameter type annotation is called with an incorrectly typed argument for that parameter, and it rejects programs where a circuit with a return-type annotation returns an incorrectly typed value. If an optional type annotation is omitted, the compiler attempts to infer a type and it rejects the program if no such type can be inferred.
Types consist of built-in primitive types, ledger-state types, program-defined types,
and references to generic type parameters in scope.
When the term "type" occurs in this document without any other qualifier, it means
either a primitive type, ledger-state type, a program-defined type, or a generic type
parameter in scope.
The use of ledger-state types is, at present, limited to typing the result of
default<T> to obtain the default value of type T, and only constant bindings
can have a ledger-state type.
A generic type is not a valid type and so cannot, for example, be used as the type of a parameter or return value. Any attempt to do so is a static error. As with any other generic entity, it must be specialized at the point of use.
Primitive types
The following are the primitive types of Compact:
| type | ⟶ | tref |
| | | Boolean | |
| | | Field | |
| | | Uint < tsize > | |
| | | Uint < tsize .. tsize > | |
| | | Bytes < tsize > | |
| | | Opaque < str > | |
| | | Vector < tsize , type > | |
| | | [ type , ⋯ , type ,opt ] | |
| tref | ⟶ | id gargsopt |
| tsize | ⟶ | nat |
| | | id |
-
Booleanis the type of Boolean values. There are only two values ofBooleantype. They are the values of the expressionstrueandfalse. -
Uint<m..n>, where m is the literal0or generic natural-number parameter bound to0, and where n is a non-zero natural number literal or a generic natural-number parameter bound to a non-zero natural number, is the type of bounded unsigned integer values between0(inclusive) andn(exclusive). (While the lower bound is currently required to be0, this restriction might be lifted at some point.)Uinttypes with different upper bounds are different types, although the one with the lower bound is a subtype of the other. The compiler and run-time system might impose a limit on the range of supported unsigned integer values. If so, it is a static error whenever aUinttype includes values that exceed this limit. The current limit, if any, is given in Implementation-specific limits. -
Uint<n>, where n is a non-zero natural number literal or generic natural-number parameter bound to a non-zero natural number, is the type of sized unsigned integer values with binary representations using up to n bits. This is the same type asUint<0..m>where m is equal to 2n. Sized integer types can be seen as a convenience for programmers.Uint<32>, for example, can be more obvious and less error-prone than the equivalentUint<0..4294967296>. Any Compact program that uses sized integer types can be rewritten to one that uses only bounded integer types, but the converse is not true. -
Fieldrepresents the set of unsigned integers with values up to the order of the native prime field of the ZK proving system. The current maximum field value is given in Implementation-specific limits. -
[T1, ⋯, Tn], where T1, ⋯, Tn are zero or more comma-separated types, is the type of tuple values with element types T1, ⋯, Tn. Tuples are heterogeneous: any element type can differ from any of the others. The length of a tuple type is the number n of element types. Two tuple types with different lengths are different types. Two tuple types where any element type of one differs from the corresponding element type of the other are also different types, though one of the tuple types might be a subtype of the other. -
Vector<n, T>, where n is a natural number literal or generic natural-number parameter and T is a type, is a shorthand notation for the tuple type[T, ⋯, T]with n occurrences of the type T. Note that a vector type and the corresponding tuple type are two different ways of writing exactly the same type. Unless otherwise specified, type rules for vector types are derived from the rules for the corresponding tuple type. -
Bytes<n>, where n is a natural number literal or a generic natural-number parameter, is the type of byte vectors of length n.Bytestypes with different lengths are different types.Bytestypes are used in the Compact standard library for hashing. String literals in Compact also have aBytestype, where n is the number of bytes in the UTF-8 encoding of the string. -
Opaque<s>, where s is a string literal, is the type of opaque values with tag s.Opaquetypes with different tags are different types. Opaque values can be manipulated in witnesses but they are opaque to circuits. They are represented in circuits as their hash. The only tags currently allowed are"string"and"Uint8Array".
Program-defined types
Programs can define three kinds of new types: structures, enumerations, and contracts. They can also define structural and nominal aliases for existing types.
Structure types
Structure types are defined via struct declarations with the following form:
| struct-declaration | ⟶ | exportopt struct struct-name gparamsopt { typed-id ; ⋯ ; typed-id ;opt } ;opt |
| | | exportopt struct struct-name gparamsopt { typed-id , ⋯ , typed-id ,opt } ;opt | |
| typed-id | ⟶ | id : type |
A structure declaration has a sequence of named fields which must be separated either by commas or by semicolons. Comma and semicolon separators cannot be mixed within a single structure declaration. A trailing separator is allowed, but not required.
Each structure field must have a type annotation. Here are a couple of examples:
struct Thing {
triple: Vector<3, Field>,
flag: Boolean,
}
struct NumberAnd<T> {
num: Uint<32>;
item: T
}
The first declaration introduces a structure type named Thing with two fields:
triple (a vector with three Field elements) and flag (a Boolean).
The second introduces a generic structure type named NumberAnd with generic
parameter T and two fields: num (a 32-bit unsigned integer) and item
(a value of type T).
Generic structure types are not fixed types and must eventually be specialized
by supplying generic arguments, e.g., NumberAnd<Uint<8>>.
When any generic structure type is specialized, it must be fully specialized:
the number of supplied generic arguments must match the number of declared
generic parameters.
The effect of specializing a generic structure type is to produce the same type
as one in which the generic parameters are replaced by the generic argument
values.
For example, NumberAnd<Uint<8>> is equivalent to NumberAnd if NumberAnd
had been defined by:
struct NumberAnd {
num: Uint<32>;
item: Uint<8>
}
It is possible and common for a generic structure type to be specialized via different generic arguments to produce different specialized structure types in different parts of a program.
Structure typing is always nominal: two types are equivalent only if they have
the same names and same fields.
They are distinct if they have different names even if they have the same fields.
More precisely: each structure type is the same as any other structure type
that has the same name, same element names (in the same order), and same element
types (in the same order).
It is distinct from every other type.
This means, for example, that the following program is well-typed:
module M {
struct NumberAnd {
num: Uint<32>;
item: Uint<8>
}
export circuit bar(x: NumberAnd): NumberAnd {
return x;
}
}
import M;
struct NumberAnd<T> {
num: Uint<32>;
item: T
}
export circuit foo(x: NumberAnd<Uint<8>>): NumberAnd<Uint<8>> {
return bar(x);
}
Structure types must not be recursive, i.e., they cannot contain elements of the same type as the structure, either directly or indirectly. An attempt to define a recursive structure type is a static error. For example, it is a static error to use the following pair of declarations:
struct Even {
predecessor: Odd
}
struct Odd {
predecessor: Even
}
export circuit doesntWork(s: Even): Odd {
return s.predecessor;
}
Values of structure types are created with structure-creation expressions and accessed via structure-field-access expressions.
Enumeration types
Enumeration types are defined via enum declarations with the following form:
An enumeration declaration has a non-empty sequence of named elements separated by commas. A trailing comma is allowed but not required.
An enumeration declaration introduces a named enumeration type, such as Arrow
in the example below:
enum Arrow { up, down, left, right };
Within the scope of this declaration, a value of type Arrow can have one of
four values, selected via Arrow.up, Arrow.down, Arrow.left, and Arrow.right.
Two enumeration types are the same if they have the same name and the same element names (in the same order) and distinct otherwise.
Contract types
Compact 1.0 does not fully implement declarations of contracts and the
cross-contract calls they support, but the keyword contract used to declare
contracts is reserved for this use.
Type aliases
Type aliases can be created via type declarations of the form:
Within the scope of a type-alias declaration of type-name for type, type-name
is itself a type.
Type aliases are either structural or nominal, depending on whether the optional
new modifier is present:
- A type alias type-name for type declared without the optional
newmodifier is a structural type alias, i.e., type-name is the same type and is fully interchangeable with type. - A type alias type-name for type declared with the optional
newmodifier is a nominal type alias, i.e., type-name is a distinct type compatible with type but neither a subtype of nor a supertype of type (or any other type).
Any nominal type alias type-name for some type type is compatible with type in the following senses:
- values of type type-name have the same representation as values of type type
- values of type type-name can be used by primitive operations that require a value of type type
- values of type type-name can be cast explicitly to type, and
- values of type type can be cast explicitly to type type-name.
For example, within the scope of
new type V3U16 = Vector<3, Uint<16>>
a value of type V3U16 can be referenced or sliced just like a vector
of type Vector<3, Uint<16>>, but it cannot, for example, be passed to a function
that expects a value of type Vector<3, Uint<16>> without an explicit cast.
When one operand of an arithmetic operation (e.g., +) receives a value of some
nominal type alias type-name, the other operand must also be of type type-name,
and the result of performing the operation is cast to type type-name.
This might cause a dynamic error if the result cannot be represented by type
type-name.
Values of any nominal type alias type-name cannot be compared directly using,
e.g., <, or ==, with values of any other type, including with values of
type type.
Such comparisons require one of the operands to be cast to the type of the other.
Both structural and nominal type aliases can take generic parameters, e.g.:
type V3<T> = Vector<3, T>;
and
new type VField<#N> = Vector<N, Field>;
When a generic nominal type is specialized, the specialized type is a nominal type.
Subtyping and least upper bounds
Some Compact types are related to other types via subtyping.
Informally, if a type T is a subtype of a type S (equivalently, S is a
supertype of type T), then every value of type T is also a value of type S,
i.e., any value of type T can be used where a value of type S is expected without
the need for an explicit cast.
For example, a circuit or witness can be called with argument expressions whose
types are subtypes of the corresponding parameter type annotations, and
a const binding statement with a type annotation can be given a value with an
expression whose type is a subtype of the type annotation.
Subtyping is exclusively defined by the following rules:
- Any type T is a subtype of itself (subtyping is reflexive)
Uint<0..n>is a subtype ofUint<0..m>` if n is less than (or equal to) mUint<0..n>is a subtype ofFieldif n-1 is less than or equal to the maximum field value. (WhetherFieldis a subtype ofUint<0..n>if n-1 is greater than the maximum field value is currently unspecified.)- The tuple type
[T1, ⋯, Tn]is a subtype of the tuple type[S1, ⋯, Sn]if they have the same length and each type Ti is a subtype of the corresponding type Si.
The least upper bound (with respect to subtyping) of a non-empty set of types {T1, ⋯, Tn} is a type S such that:
- S is an upper bound: Ti is a subtype of S for all i in the range 1..n, and
- S is the least upper bound: for all upper bounds R of the set of types {T1, ⋯, Tn}, S is a subtype of R.
Note that least upper bounds do not exist for all sets of types, because
some types (such as Boolean and Field) are not related.
Tuple and vector types:
Every vector type is equivalent to some tuple type.
Specifically, as noted in the earlier section on Primitive types,
the vector type Vector<n, T> is equivalent to the tuple type
[T, ⋯, T] with n occurrences of T.
Thus the above subtyping rule for tuple types applies to vector types as well:
every vector type is a subtype of the equivalent tuple type and possibly of some
other tuple and vector types.
In general, a vector type Vector<n, T> is a subtype of a tuple type
[S1, ⋯, Sn] if T is a subtype of each of the
types S1, ⋯, Sn.
This means, for instance, that a vector can often be passed to a circuit where a
tuple is expected.
On the other hand, tuple types do not always have equivalent vector types.
For example, neither [Boolean, Field] nor [Uint<8>, Uint<16>] is
equivalent to any vector type.
We say, however, that a tuple type [T1, ⋯, Tn] with
possibly distinct types T1, ⋯, Tn "has a vector type" if the
least upper bound S of the set of types {T1, ⋯, Tn}
exists.
In that case, the tuple type has the vector type Vector<n, S>.
Some operations over tuples (such as mapping and folding) require the tuple type
to have a vector type.
When a tuple type has a vector type, the tuple type is a subtype of the vector
type, but it might not be the same as the vector type.
For example, [Uint<16>, Uint<16>] has the vector type Vector<2, Uint<16>>,
and the two types are the same, whereas [Uint<8>, Uint<16>] also has the
vector type Vector<2, Uint<16>>, but the types are not the same.
Patterns and destructuring
The parameters of a circuit or constructor and the target of a const binding
are specified via patterns:
| pattern | ⟶ | id |
| | | [ patternopt , ⋯ , patternopt ,opt ] | |
| | | { pattern-struct-elt , ⋯ , pattern-struct-elt ,opt } | |
| pattern-struct-elt | ⟶ | id |
| | | id : pattern |
In its simplest form, a pattern is just an identifier.
For example, in the code below, the parameter of sumTuple is the identifier
x and the targets of the two const bindings are the identifiers a and b.
circuit sumTuple(x: [Field, Field]): Field {
const a = x[0], b = x[1];
return a + b;
}
When the parameter type is a tuple, vector, or struct, it is often convenient to use one of the destructuring forms of patterns to name individual pieces of the tuple or struct at the point of binding rather than extracting them at each point of use. For example, one could replace the above with:
circuit sumTuple(x: [Field, Field]): Field {
const [a, b] = x;
return a + b;
}
or more simply with:
circuit sumTuple([a, b]: [Field, Field]): Field {
return a + b;
}
Here is a similar example that destructures a struct instead of a tuple:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumStruct({x, y}: S): Uint<64> {
return x + y;
}
Whereas the elements of a tuple pattern are necessarily given in order, the
elements of a struct pattern need not be consistent with the order of the fields
in the declaration.
For example, the definition of sumStruct below is equivalent to the one above,
even though the order of the pattern elements has been swapped:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumStruct({y, x}: S): Uint<64> {
return x + y;
}
By default, the names bound by the pattern are the same as the names of the structure elements. When this is not convenient, it is possible to choose different names for the structure elements:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumStruct({x: a, y}: S): Uint<64> {
return a + y;
}
While x: a looks like an identifier with a type annotation, in this context
it simply indicates that a rather than x is bound to the value in the
x field.
Patterns can be arbitrarily nested, e.g.:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumTupleStruct([{x: a1, y: b1}, {x: a2, y: b2}]: [S, S]): Uint<64> {
return a1 + b1 + a2 + b2;
}
It is permissible and sometimes useful to not name certain parts of the tuple or struct.
struct S { x: Uint<16>, y: Uint<32> }
circuit sumSomeYs([{y: b1}, , {y: b3}]: [S, S, S]): Uint<64> {
return b1 + b3;
}
Here the input is a tuple with three elements, but the pattern skips the
second by putting two commas between the first and third.
Similarly, while each element of the tuple is a struct with both x and y
fields, the pattern ignores the x fields simply by failing to mention them.
It is a static error if a pattern implies a different shape from the declared or inferred type of value to be destructured. For example:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumStruct({x, y}: [Uint<16>, Uint<32>]): Uint<64> {
return x + y;
}
fails because it tries to treat a tuple as a struct, while:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumSomeYs([{y: b1}, , , {y: b3}]: [S, S, S]): Uint<64> {
return b1 + b3;
}
fails because it implies that the input tuple has four elements (including two skipped elements) when it actually has only three, and:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumSomeYs([{y: b1}, , {z: b3}]: [S, S, S]): Uint<64> {
return b1 + b3;
}
fails because it tries to name a nonexistent z field in one of the structs.
Trailing commas in a pattern imply nothing about the structure of the input and are ignored:
struct S { x: Uint<16>, y: Uint<32> }
circuit sumSomeYs([{y: b1,}, , {y: b3,},]: [S, S, S]): Uint<64> {
return b1 + b3;
}
Programs
A compact program is a sequence of zero or more program elements.
| program |