fpp/docs/users-guide/Defining-Types.adoc
2025-11-02 12:37:46 -08:00

714 lines
22 KiB
Plaintext

== Defining Types
An FPP model may include one or more *type definitions*.
These definitions describe named types that may be used elsewhere in the
model and that may generate code in the target language.
For example,
an FPP type definition may become a class definition in {cpp}.
There are four kinds of type definitions:
* Array type definitions
* Struct type definitions
* Abstract type definitions
* Alias type definitions
Type definitions may appear at the top level or inside a
<<Defining-Modules,module definition>>.
A type definition is an
<<Writing-Comments-and-Annotations_Annotations,annotatable element>>.
=== Array Type Definitions
An *array type definition* associates a name with an *array type*.
An array type describes the shape of an
<<Defining-Constants_Expressions_Array-Values,array value>>.
It specifies an element type and a size.
==== Writing an Array Type Definition
As an example, here is an array type definition that associates
the name `A` with an array of three values, each of which is a 32-bit unsigned
integer:
[source,fpp]
----
array A = [3] U32
----
In general, to write an array type definition, you write the following:
* The keyword `array`.
* The <<Defining-Constants_Names,name>> of the array type.
* An equals sign `=`.
* An <<Defining-Constants_Expressions,expression>>
enclosed in square brackets `[` ... `]` denoting the size (number of elements) of the array.
* A type name denoting the element type.
The available type names are discussed below.
Notice that the size expression precedes the element type, and the whole
type reads left to right.
For example, you may read the type `[3] U32` as "array of 3 `U32`."
The size may be any legal expression.
It doesn't have to be a literal integer.
For example:
[source,fpp]
----
constant numElements = 10
array A = [numElements] U32
----
==== Type Names
The following type names are available for the element types:
* The type names `U8`, `U16`, `U32`, and `U64`, denoting the type of unsigned
integers of width 8, 16, 32, and 64 bits.
* The type names `I8`, `I16`, `I32`, and `I64`, denoting the type of signed integers
of width 8, 16, 32, and 64 bits.
* The type names `F32` and `F64`, denoting the type of floating-point values
of width 32 and 64 bits.
* The type name `bool`, denoting the type of Boolean values (`true` and `false`).
* The type name `string`, denoting the type of
<<Defining-Constants_Expressions_String-Values,string values>>.
This type has a default maximum size.
For example:
+
[source,fpp]
----
# A is an array of 3 strings with default maximum size
array A = [3] string
----
* The type name `string size` _e_, where _e_ is a numeric expression
specifying a maximum string size.
+
[source,fpp]
----
# A is an array of 3 strings with maximum size 40
array A = [3] string size 40
----
* A name associated with another type definition.
In particular, an array definition may have another array definition as
its element type; this situation is discussed further below.
An array type definition may not refer to itself (array type definitions are not
recursive). For example, this definition is illegal:
[source,fpp]
--------
array A = [3] A # Illegal: the definition of A may not refer to itself
--------
==== Default Values
Optionally, you can specify a default value for an array type.
To do this, you write the keyword `default` and an expression
that evaluates to an <<Defining-Constants_Expressions_Array-Values,array value>>.
For example, here is an array type `A` with default value `[ 1, 2, 3 ]`:
[source,fpp]
----
array A = [3] U32 default [ 1, 2, 3 ]
----
A default value expression need not be a literal array value; it
can be any expression with the correct type.
For example, you can create a named constant with an array
value and use it multiple times, like this:
[source,fpp]
----
constant a = [ 1, 2, 3 ]
array A = [3] U8 default a # default value is [ 1, 2, 3 ]
array B = [3] U16 default a # default value is [ 1, 2, 3 ]
----
If you don't specify a default value, then the type gets an automatic default value,
consisting of the default value for each element.
The default numeric value is zero, the default Boolean value is `false`,
the default string value is `""`, and the default value of an array type
is specified in the type definition.
The type of the default expression must match the size and element type of the
array, with type conversions allowed as discussed for
<<Defining-Constants_Expressions_Array-Values,array values>>.
For example, this default expression is allowed, because we can convert integer
values to floating-point values, and we can promote a single value to an array
of three values:
[source,fpp]
----
array A = [3] F32 default 1 # default value is [ 1.0, 1.0, 1.0 ]
----
However, these default expressions are not allowed:
[source,fpp]
--------
array A = [3] U32 default [ 1, 2 ] # Error: size does not match
--------
[source,fpp]
--------
array B = [3] U32 default [ "a", "b", "c" ] # Error: element type does not match
--------
==== Format Strings
You can specify an optional *format string* which says how to display
each element value and optionally provides some surrounding text.
For example, here is an array definition that interprets three integer
values as wheel speeds measured in RPMs:
[source,fpp]
----
array WheelSpeeds = [3] U32 format "{} RPM"
----
Then an element with value 100 would have the format `100 RPM`.
Note that the format string specifies the format for an _element_, not the
entire array.
The way an entire array is displayed is implementation-specific.
A standard way is a comma-separated list enclosed in square brackets.
For example, a value `[ 100, 200, 300 ]` of type `WheelSpeeds` might
be displayed as `[ 100 RPM, 200 RPM, 300 RPM ]`.
Or, since the format is the same for all elements, the implementation could
display the array as `[ 100, 200, 300 ] RPM`.
The special character sequence `{}` is called a *replacement field*; it says
where to put the value in the format text.
Each format string must have exactly one replacement field.
The following replacement fields are allowed:
* The field `{}` for displaying element values in their default format.
* The field `{c}` for displaying a character value
* The field `{d}` for displaying a decimal value
* The field `{x}` for displaying a hexadecimal value
* The field `{o}` for displaying an octal value
* The field `{e}` for displaying a rational value in exponent notation, e.g.,
`1.234e2`.
* The field `{f}` for displaying a rational value in fixed-point notation,
e.g., `123.4`.
* The field `{g}` for displaying a rational value in general format
(fixed-point notation up to an implementation-dependent size and exponent
notation for larger sizes).
For field types `c`, `d`, `x`, and `o`, the element type must be an integer
type.
For field types `e`, `f`, and `g`, the element type must be a floating-point
type.
For example, the following format string is illegal, because
type `string` is not an integer type:
[source,fpp]
--------
array A = [3] string format "{d}" # Illegal: string is not an integer type
--------
For field types `e`, `f`, and `g`, you can optionally specify a precision
by writing a decimal point and an integer before the field type. For example,
the replacement field `{.3f}`, specifies fixed-point notation with a precision
of 3.
To include the literal character `{` in the formatted output, you can write
`{{`, and similarly for `}` and `}}`. For example, the following definition
[source,fpp]
----
array A = [3] U32 format "{{element {}}}"
----
specifies a format string `element {0}` for element value 0.
No other use of `{` or `}` in a format string is allowed. For example, this is illegal:
[source,fpp]
--------
array A = [3] U32 format "{" # Illegal use of { character
--------
You can include both a default value and a format; in this case, the default
value must come first. For example:
[source,fpp]
----
array WheelSpeeds = [3] U32 default 100 format "{} RPM"
----
If you don't specify an element format, then each element is displayed
using the default format for its type.
Therefore, omitting the format string is equivalent to writing the format
string `"{}"`.
==== Arrays of Arrays
An array type may have another array type as its element type.
In this way you can construct an array of arrays.
For example:
[source,fpp]
----
array A = [3] U32
array B = [3] A # An array of 3 A, which is an array of 3 U32
----
When constructing an array of arrays, you may provide any legal
default expression, so long as the types are compatible.
For example:
[source,fpp]
----
array A = [2] U32 default 10 # default value is [ 10, 10 ]
array B1 = [2] A # default value is [ [ 10, 10 ], [ 10, 10 ] ]
array B2 = [2] A default 1 # default value is [ [ 1, 1 ], [ 1, 1 ] ]
array B3 = [2] A default [ 1, 2 ] # default value is [ [ 1, 1 ], [ 2, 2 ] ]
array B4 = [2] A default [ [ 1, 2 ], [ 3, 4 ] ]
----
=== Struct Type Definitions
A *struct type definition* associates a name with a *struct type*.
A struct type describes the shape of a
<<Defining-Constants_Expressions_Struct-Values,struct value>>.
It specifies a mapping from element names to their types.
As discussed below, it also specifies a serialization order
for the struct elements.
==== Writing a Struct Type Definition
As an example, here is a struct type definition that associates the name `S` with
a struct type containing two members: `x` of type `U32`, and `y` of type `string`:
[source,fpp]
----
struct S { x: U32, y: string }
----
In general, to write a struct type definition, you write the following:
* The keyword `struct`.
* The <<Defining-Constants_Names,name>> of the struct type.
* A sequence of *struct type members* enclosed in curly braces `{` ... `}`.
A struct type member consists of a name, a colon, and a
<<Defining-Types_Array-Type-Definitions_Type-Names,type name>>,
for example `x: U32`.
The struct type members form an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,element
sequence>>
in which the optional terminating punctuation is a comma.
As usual for element sequences, you can omit the comma and use
a newline instead.
So, for example, we can write the definition shown above in this alternate way:
[source,fpp]
----
struct S {
x: U32
y: string
}
----
==== Annotating a Struct Type Definition
As noted in the beginning of this section, a type definition is
an annotatable element, so you can attach pre and post annotations
to it.
A struct type member is also an annotatable element, so any
struct type member can have pre and post annotations as well.
Here is an example:
[source,fpp]
----
@ This is a pre annotation for struct S
struct S {
@ This is a pre annotation for member x
x: U32 @< This is a post annotation for member x
@ This is a pre annotation for member y
y: string @< This is a post annotation for member y
} @< This is a post annotation for struct S
----
==== Default Values
You can specify an optional default value for a struct definition.
To do this, you write the keyword `default` and an expression
that evaluates to a <<Defining-Constants_Expressions_Struct-Values,struct
value>>.
For example, here is a struct type `S` with default value `{ x = 1, y = "abc"
}`:
[source,fpp]
----
struct S { x: U32, y: string } default { x = 1, y = "abc" }
----
A default value expression need not be a literal struct value; it
can be any expression with the correct type.
For example, you can create a named constant with a struct
value and use it multiple times, like this:
[source,fpp]
----
constant s = { x = 1, y = "abc" }
struct S1 { x: U8, y: string } default s
struct S2 { x: U32, y: string } default s
----
If you don't specify a default value, then the struct type gets an automatic default
value,
consisting of the default value for each member.
The type of the default expression must match the type of the struct, with type
conversions allowed as discussed for
<<Defining-Constants_Expressions_Struct-Values,struct values>>.
For example, this default expression is allowed, because we can convert integer
values to floating-point values, and we can promote a single value to a
struct with numeric members:
[source,fpp]
----
struct S { x: F32, y: F32 } default 1 # default value is { x = 1.0, y = 1.0 }
----
And this default expression is allowed, because if we omit a member of a struct,
then FPP will fill in the member and give it the default value:
[source,fpp]
----
struct S { x: F32, y: F32 } default { x = 1 } # default value is { x = 1.0, y = 0.0 }
----
However, these default expressions are not allowed:
[source,fpp]
--------
struct S1 { x: U32, y: string } default { z = 1 } # Error: member z does not match
--------
[source,fpp]
--------
struct S2 { x: U32, y: string } default { x = "abc" } # Error: type of member x does not match
--------
==== Member Arrays
For any struct member, you can specify that the member
is an array of elements.
To do this you, write an array the size enclosed in square brackets
before the member type.
For example:
[source,fpp]
----
struct S {
x: [3] U32
}
----
This definition says that struct `S` has one element `x`,
which is an array consisting of three `U32` values.
We call this array a *member array*.
*Member arrays vs. array types:*
Member arrays let you include an array
of elements as a member of a struct type,
without defining a separate
<<Defining-Types_Array-Type-Definitions,named array type>>.
Also, member arrays generate less code than named arrays.
Whereas a member size array is a native {cpp} array,
each named array is a {cpp} class.
On the other hand, defining a named array is usually
a good choice when
* You want to use the array outside of any structure.
* You want the convenience of a generated array class,
which has a richer interface than the bare
{cpp} array.
In particular, the generated array class provides
*bounds-checked* access operations:
it causes a runtime failure if an out-of-bounds access
occurs.
The bounds checking provides an additional degree of memory
safety when accessing array elements.
*Member arrays and default values:*
FPP ignores member array sizes when checking the types of
default values.
For example, this code is accepted:
[source,fpp]
----
struct S {
x: [3] U32
} default { x = 10 }
----
The member `x` of the struct `S` gets three copies of the value
10 specified for `x` in the default value expression.
==== Member Format Strings
For any struct member, you can include an optional format.
To do this, write the keyword `format` and a format string.
The format string for a struct member has the same form as for an
<<Defining-Types_Array-Type-Definitions_Format-Strings,array member>>.
For example, the following struct definition specifies
that member `x` should be displayed as a hexadecimal value:
[source,fpp]
----
struct Channel {
name: string
offset: U32 format "offset 0x{x}"
}
----
How the entire struct is displayed depends on the implementation.
As an example, the value of `S` with `name = "momentum"` and `offset = 1024`
might look like this when displayed:
----
Channel { name = "momentum", offset = 0x400 }
----
If you don't specify a format for a struct member, then the system uses the default
format for the type of that member.
If the member has a size greater than one, then the format
is applied to each element.
For example:
[source,fpp]
----
struct Telemetry {
velocity: [3] F32 format "{} m/s"
}
----
The format string is applied to each of the three
elements of the member `velocity`.
==== Struct Types Containing Named Types
A struct type may have an array or struct type as a member type.
In this way you can define a struct that has arrays or structs as members.
For example:
[source,fpp]
----
array Speeds = [3] U32
# Member speeds has type Speeds, which is an array of 3 U32 values
struct Wheel { name: string, speeds: Speeds }
----
When initializing a struct, you may provide any legal
default expression, so long as the types are compatible.
For example:
[source,fpp]
----
array A = [2] U32
struct S1 { x: U32, y: string }
# default value is { s1 = { x = 0, y = "" }, a = [ 0, 0 ] }
struct S2 { s1: S1, a: A }
# default value is { s1 = { x = 0, y = "abc" }, a = [ 5, 5 ] }
struct S3 { s1: S1, a: A } default { s1 = { y = "abc" }, a = 5 }
----
==== The Order of Members
For <<Defining-Constants_Expressions_Struct-Values,struct values>>,
we said that the order in which the members appear in the value is not
significant.
For example, the expressions `{ x = 1, y = 2 }` and `{ y = 2, x = 1 }` denote
the same value.
For struct types, the rule is different.
The order in which the members appear is significant, because
it governs the order in which the members appear in the generated
code.
For example, the type `struct S1 { x: U32, y : string }` might generate a {cpp}
class `S1` with members `x` and `y` laid out with `x` first; while `struct S2
{ y : string, x : U32 }`
might generate a {cpp} class `S2` with members `x` and `y` laid out with `y`
first.
Since class members are generally serialized in the order in which they appear in
the class,
the members of `S1` would be serialized with `x` first, and the members of
`S2`
would be serialized with `y` first.
Serializing `S1` to data and then trying to deserialize it to `S2` would
produce garbage.
The order matters only for purposes of defining the type, not for
assigning default values to it.
For example, this code is legal:
[source,fpp]
----
struct S { x: U32, y: string } default { y = "abc", x = 5 }
----
FPP struct _values_ have no inherent order associated with their members.
However, once those values are assigned to a named struct _type_,
the order becomes fixed.
=== Abstract Type Definitions
An array or struct type definition specifies a complete type:
in addition to the name of the type, it provides the names and types
of all the members.
An *abstract type*, by contrast, has an incomplete or opaque definition.
It provides only a name _N_.
Its purpose is to tell the analyzer that a type with name _N_ exists and will
be defined elsewhere.
For example, if the target language is {cpp}, then the type is a {cpp}
class.
To define an abstract type, you write the keyword `type` followed
by the name of the type.
For example, you can define an abstract type `T`; then you can construct
an array `A` with member type `T`:
[source,fpp]
----
type T # T is an abstract type
array A = [3] T # A is an array of 3 values of type T
----
This code says the following:
* A type `T` exists. It is defined in the implementation,
but not in the model.
* `A` is an array of three values, each of type `T`.
Now suppose that the target language is {cpp}.
Then the following happens when generating code:
* The definition `type T` does not cause any code to be generated.
* The definition `array A =` ... causes a {cpp} class `A`
to be generated.
By F Prime convention, the generated files are `AArrayAc.hpp` and `AArrayAc.cpp`.
* File `AArrayAc.hpp` includes a header file `T.hpp`.
It is up to the user to implement a {cpp} class `T` with
a header file `T.hpp`.
This header file must define `T` in a way that is compatible
with the way that `T` is used in `A`.
We will have more to say about this topic in the section on
<<Writing-C-Plus-Plus-Implementations_Implementing-Abstract-Types,
implementing abstract types>>.
In general, an abstract type `T` is opaque in the FPP model
and has no values that are expressible in the model.
Thus, every use of an abstract type `T` represents the default value
for `T`.
The implementation of `T` in the target language
provides the default value.
In particular, when the target language is {cpp}, the default
value is the zero-argument constructor `T()`.
=== Alias Type Definitions
An *alias type definition* provides an alternate name for
a type that is defined elsewhere.
The alternate name is called an *alias* of the original type.
For example, here is an alias type definition specifying that the type
`T` is an alias of the type `U32`:
[source,fpp]
----
type T = U32
----
Wherever this definition is available, the type `T` may be used
interchangeably with the type `U32`.
For example:
[source,fpp]
----
type T = U32
array A = [3] T
----
An alias type definition may refer to any type, including another
alias type, except that it may not refer to itself, either directly or through
another alias.
For example, here is an alias type definition specifying that the type
`T` is an alias of the struct type `S`:
[source,fpp]
----
struct S { x: U32, y: I32 }
type T = S
----
Here is a pair of definitions specifying that `S` is an alias of `T`,
and `T` is an alias of `F32`:
[source,fpp]
----
type S = T
type T = F32
----
Here is a pair of alias type definitions that is illegal, because each of
the definitions indirectly refers to itself:
[source,fpp]
--------
type S = T
type T = S
--------
Alias type definitions are useful for specifying configurations.
Part of a model can use a type `T`, without
defining `T`; elsewhere, configuration code can define
`T` to be the alias of another type.
F Prime uses this method to provide basic type whose definitions
configure the framework.
These types are defined in the file `config/FpConfig.fpp`.
=== Framework Types
Certain types defined in FPP have a special meaning in the F Prime
framework.
These types are called *framework types*.
For example, the type `FwOpcodeType` defines the type of
a command opcode.
(Commands are an F Prime feature that we describe
<<Defining-Components_Commands,in a later section of this manual>>.)
Framework types are like <<Defining-Constants_Framework-Constants,framework
constants>>:
you typically define them by overriding configuration files
provided by F Prime, and if they are defined, then they must
conform to certain rules.
https://nasa.github.io/fpp/fpp-spec.html#Definitions_Framework-Definitions_Type-Definitions[_The
FPP Language Specification_] has all the details.