Product Type

The product type is a compound type composed of none or more internal types.

Signature

<struct_field_signature> ::= <ident> ":" <type_signature> ;
<struct_signature> ::=
    ["packed"] "{"
        [<struct_field_signature> ("," <struct_field_signature>)* [","]]
    "}" ;
<tuple_signature> ::= ["packed"] "(" <type_signature> ("," <type_signature>)* [","] ")" ;

Dependencies:

Instantiation

<struct_field_instantiation> ::= <ident> ":" <expr> ;

<struct_instantiation> ::=
    [<data_location>] <struct_signature> "{"
        [<struct_field_instantiation> ("," <struct_field_instantiation>)* [","]]
    "}" ;

<tuple_instantiation> ::= [<data_location>] <ident> "(" [<expr> ("," <expr>)* [","]] ")" ;

Dependencies:

The <struct_instantiation> is an instantiation, or creation, of a struct. It may optionally include a data location annotation, however the semantic rules for this are in the data location semantic rules. It is instantiated by the struct identifier followed by a comma separated list of field name and value pairs delimited by curly braces.

The <tuple_instantiation> is an instantiation, or creation, of a tuple. It may optionally include a data location annotation, however the semantic rules for this are in the data location semantic rules. It is instantiated by a comma separated list of expressions delimited by parenthesis.

Field Access

<struct_field_access> ::= <ident> "." <ident> ;
<tuple_field_access> ::= <ident> "." <dec_char> ;

Dependencies:

The <struct_field_access> is written as the struct's identifier followed by the field's identifier separated by a period.

The <tuple_field_access> is written as the tuple's identifier followed by the field's index separated by a period.

Examples

type PrimitiveStruct = {
    a: u8,
    b: u8,
    c: u8,
};

const primitiveStruct: PrimitiveStruct = PrimitiveStruct { a: 1, b: 2, c: 3 };

const a = primitiveStruct.a;

type PackedTuple = packed (u8, u8, u8);

const packedTuple: PackedTuple = (1, 2, 3);

const one = packedTuple.0;

Semantics

The struct field signature maps a type identifier to a type signature. The field may be accessed by the struct's identifier and field identifier separated by a dot.

Prefixing the signature with the "packed" keyword will pack the fields by their bitsize, otherwise each field is padded to its own 256 bit word.

type Rgb = packed { r: u8, g: u8, b: u8 };

let rgb = Rgb { r: 1, g: 2, b: 3 };
// rbg = 0x010203

Instantiation depends on the data location. Structs that can fit into a single word, either a single field struct or a packed struct with a bitsize sum less than or equal to 256, sit on the stack by default. Instantiating a struct in memory requires the memory data location annotation. If a struct that does not fit into a single word does not have a data location annotation, a compiler error is thrown.

Stack struct instantiation consists of optionally bitpacking fields and leaving the struct on the stack. Memory instantiation consists of allocating new memory, optionally bitpacking fields, storing the struct in memory, and leaving the pointer to it on the stack.

type MemoryRgb = { r: u8, g: u8, b: u8 };

let memoryRgb = MemoryRgb{ r: 1, g: 2, b: 3 };
// ptr = ..
// mstore(ptr, 1)
// mstore(add(32, ptr), 2)
// mstore(add(64, ptr), 3)

Persistent and transient storage structs must be instantiated at the file level. If anything except zero values are assigned, storage writes will be injected into the initcode to be run on deployment. A reasonable convention for creating a storage layout without the contract object abstraction would be to create a Storage type which is a struct, mapping identifiers to storage slots. Nested structs will also allow granular control over which variables get packed.

type Storage = {
    a: u8,
    b: u8,
    c: packed {
        a: u8,
        b: u8
    }
}

const storage = @default<Storage>();

fn main() {
    storage.a = 1;      // sstore(0, 1)
    storage.b = 2;      // sstore(1, 2)
    storage.c.a = 3;    // ca = shl(8, 3)
    storage.c.b = 4;    // sstore(2, or(ca, 4))
}

Packing rules for buffer locations is to pack everything exactly by its bit length. Packing rules for map locations is to right-align the first field, for each subsequent field, if its bitsize fits into the same word as the previous, it is left-shifted to the first available bits, otherwise, if the bitsize would overflow, it becomes a new word.

type Storage = {
    a: u128,
    b: u8,
    c: addr,
    d: u256
}

const storage = Storage {
    a: 1,
    b: 2,
    c: 0x3,
    d: 4,
};
SlotValue
0x000x0000000000000000000000000000000200000000000000000000000000000001
0x010x0000000000000000000000000000000000000000000000000000000000000003
0x020x0000000000000000000000000000000000000000000000000000000000000004