We saw that field constraints are useful to express magic numbers,
which are pretty common in binary formats. Imagine a package has a
marker byte at the beginning, which should always be 0xff
. We
could use a constraint like in:
type Packet = struct { byte marker : marker == 0xff; byte length; byte[length] payload; };
This works well when mapping packages. The constraint is checked and
a constraint violation exception is raised if the first byte of the
alleged package is not 0xff
.
However, suppose we want to construct a new Package
, with no
particular contents. We would use a constructor, but unfortunately:
(poke) Packet { } unhandled constraint violation exception
What happened? Since we didn’t specify a value for the marker in the struct constructor, a default value was used. The default value for an integral type is zero, which violates the constraint associated with the field. Therefore, we would need to remember to specify the marker, every time we construct a new packet:
(poke) Packet { marker = 0xff } Packet { marker=0xffUB, length=0x0UB, payload=[] }
Unfortunately, such markers and magic numbers are not precisely very memorable. To help with this, Poke has the notion of type field initializers. Let’s use one in our example:
type Packet = struct { byte marker = 0xff; byte length; byte[length] payload; };
Note how the syntax is different than the one used for constraints. When a field in a struct type has an initializer, the struct constructor will use the initializer expression as the initial value for the field. For example:
(poke) Packet {} Packet { marker=0xffUB, length=0x0UB, payload=[] }
It is possible to specify both a constraint and an initializer in the same field. Suppose we want to support several kinds of packets, characterized by several markers. The supported markers are however a closed set. We could do it like this:
type Packet = struct { byte marker = 0xff : marker in [0xffUB, 0xfeUB]; byte length; byte[length] payload; };
Note however that struct mappers do not make use of field
initializers, since the mapped IO space provides values for all the
fields in the struct type. If we mapped the Packet
type above,
we would need to add also a constraint to make sure the value of
marker
is the right one. This also applies to constructing
Packet
types where an initial value is explicitly specified.
We could do it by specifying both an initial value, and a constraint
expression:
type Packet = struct { byte marker = 0xff : marker == 0xff; byte length; byte[length] payload; };
This idiom is to common that Poke provides a more compact syntax to denote it, that avoids verbosity and replicated logic:
type Packet = struct { byte marker == 0xff; byte length; byte[length] payload; };
It is considered good practice to design struct types in a way that a constructor with no arguments will result in something usable.