Tutorial
Simple Record
For our first example, let us consider a very simple data structure: a record with two one-byte fields.
Field |
Type |
|---|---|
First |
|
Second |
When stored in byte-addressable memory (e.g. RAM), the data structure is layed out like this:
Byte Offset |
Content |
|---|---|
0 |
|
1 |
|
A straight-forward Tydl declaration for this data structure can be
constructed by creating a file named Simple.td containing:
1import
2 identifiers: all
3 from:
4 Tydl
5 Tydl.Data
6
7Simple: Record
8 fields:
9 First: Field
10 type: 'Unsigned Integer'
11 bit_width: 8
12 Second: Field
13 type: 'Unsigned Integer'
14 bit_width: 8
Although this is not the place for us to discuss every nuance of the Tydl syntax, it would be helpful to spend some time stepping through this declaration line-by-line.
- Lines 1-5
The first statement is an import directive that brings all the identifiers declared in the
TydlandTydl.Datanamespaces into file scope. The relevant identifiers for this example areRecord,FieldandUnsigned Integer. The syntax construct used to represent directives, referred to as a call, is also used to represent function calls, macro invocations, and data type specializations or extensions. A call consists of an identifier followed by an indented list of key-value pairs, one per line, that represent the arguments passed to the directive, function, macro, etc.1import 2 identifiers: all 3 from: 4 Tydl 5 Tydl.Data
- Line 1
The presence of an identifier followed by a series of indented lines indicates that the entity corresponding to the identifier should be called. In this case, the
importidentifier corresponds to a built-in directive, and calling it translates to executing the directive.- Line 2
The indentation indicates that this line should be interpreted an argument to the import directive. It consists of a key (i.e. an identifier followed by a colon) representing the name of the argument being specified followed by the value for that argument. In the context of the import directive, the
identifiersargument is used to indicate the identifiers that should be imported from the specified namespace. This argument may be either a single identifier or a list of identifiers. Passing in the identifierall(as is done here) is a special case that indicates everything declared in the specified namespaces should be imported.- Line 3
The presence of a key (in this case
from) followed by a series of indented lines indicates that a list is being supplied as the argument value. In general, thefromargument may be either a single (possibly dotted) identifier, or a list of identifiers indicating the source namespaces that identifiers will be imported from.- Line 4-5
List of namespaces that identifiers will be imported from. The period (
.character) is referred to as the member operator, and in this case indicates that theDatanamespace being referenced is nested under the parentTydlnamespace.- Lines 7-14
This statement declares a new specialized data Record named
Simple.7Simple: Record 8 fields: 9 First: Field 10 type: 'Unsigned Integer' 11 bit_width: 8 12 Second: Field 13 type: 'Unsigned Integer' 14 bit_width: 8
- Line 7
The presence of a key-value pair at file scope indicates that the specified value (in this case, a specialized data Record entity) should be added to the namespace associated with this file (in this case, the root namespace) under the name specified by the key (in this case
Simple). The following indented lines indicate the call construct, which in the case of a data type like Record, indicates that the arguments are to be interpreted as attributes that specialize, constrain, or extend the data type in some way. This is similar to specifying class template arguments in C++.- Lines 8
The fields attribute of a data Record is a list of key-value pairs that specify the names and properties of each member in the record. The order in which the fields are listed determines the order they are stored in memory, unless a location is explicitly specified.
- Lines 9, 12
Each value in the list of key-value pairs is typically a Field entity. The corresponding keys indicate the field names. In this case, our data record has two fields, named
FirstandSecond.- Lines 10, 13
The
typeattribute of a Field specifies the data type of the field. The use of single quotes are required to reference the names of a complex identifiers that have unusual characters (in this case, spaces) in the name. Simple aliases (e.g.UInt) for many entities are available for those who find this naming convention objectionable.- Lines 11, 14
The
bit_widthattribute of an Unsigned Integer entity specifies the number of bits used to represent the associated value.
Now that we have discussed this example in detail, you will hopefully find
the rest of the declarations in this tutorial readable, and the meaning
intuitive, even though you may not understand all the details of the syntax
at this point. Those who are curious can refer to the Wumps
documentation to find more detailed information on the low-level syntax that
serves as a foundation for the Tydl language.
It is also worth noting that although this tutorial favors the use of more verbose, explicit formatting for clarity, there are alternative, more compact ways of expressing things. The following declaration, for example, is equivalent to the one we just discussed above:
1import all from: (Tydl, Tydl.Data)
2
3Simple: Record
4 fields:
5 First: UInt 8
6 Second: UInt 8
Now that we have described our data structure using the Tydl syntax, the next
step is to use the tydl command-line tool to generate a C++ smart
structure class that implements this type:
tydl --generate=cpp_class --entity=Simple Simple.td
This results in the following directory tree being created:
└── generated
└── Simple.hpp
A C++ program that utilizes the auto-generated smart structure class for
this Simple record might look something like this:
1#include <generated/Simple.hpp>
2#include <iostream>
3
4int main()
5{
6 using namespace std;
7
8 // instantiation
9 Simple s1, s2;
10
11 // functional setters
12 s1.First(1)
13 s1.Second(2)
14
15 // chained setters
16 s1.First(1)
17 .Second(2);
18
19 // explicit setters
20 tydl::set(s1.First, 1);
21 tydl::set(s1.Second, 2);
22
23 // assignment operators
24 s2.First = 10;
25 s2.Second = s2.First;
26
27 // functional getter
28 uint8_t first = s1.First();
29
30 // explicit getter
31 uint8_t second = tydl::get(s1.Second);
32
33 cout << first << " " << second << endl;
34
35 return 0;
36}
Nested Fields
For our second example, let us consider a slightly more complex data structure: a record with nested fields.
Field |
Type |
|---|---|
S1 |
|
S2 |
This record contains two instances of the Simple data structure described
in the previous section. When stored in byte-addressable memory, the data
structure is layed out like this:
Byte Offset |
Content |
|---|---|
0 |
|
1 |
|
2 |
|
3 |
|
A Tydl declaration for this data structure can be written as follows:
1import
2 identifiers: all
3 from:
4 Tydl
5 Tydl.Data
6
7Nested: Record
8 fields:
9 S1: Field
10 type: Simple
11 S2: Field
12 type: Simple
This declaration assumes that the Tydl definition of the Simple data
structure from the previous section is also available.
The API for accessing nested fields of an auto-generated C++ smart structure class is illustrated in the following C++ program:
1#include <generated/Nested.hpp>
2#include <iostream>
3
4int main()
5{
6 using namespace std;
7
8 Nested n;
9 Simple s;
10
11 // chained setters
12 n.S1.First(1)
13 .Second(2);
14 s.First(3)
15 .Second(4);
16
17 // assignment operators
18 n.S2.First = 5;
19 n.S2.Second = n.S1.Second;
20 n.S1 = s;
21
22 // functional getters
23 uint8_t First = n.S1.First();
24 s = n.S2();
25
26 cout << First << endl;
27 return 0;
28}
Multi-Byte Fields
For our next example, let us consider another simple data structure: a record with two multi-byte fields.
Field |
Type |
|---|---|
First |
|
Second |
When stored in byte-addressable memory on a little-endian machine, the data structure is layed out like this:
Byte Offset |
Content |
|---|---|
0 |
Least-Significant Byte of |
1 |
Most-Significant Byte of |
2 |
Least-Significant Byte of |
3 |
Most-Significant Byte of |
Note that when stored in byte-addressable memory on a big-endian machine, the same data structure is layed out in a slightly different way:
Byte Offset |
Content |
|---|---|
0 |
Most-Significant Byte of |
1 |
Least-Significant Byte of |
2 |
Most-Significant Byte of |
3 |
Least-Significant Byte of |
If we want to make sure that the data structure is stored or transmitted in a consistent way, regardless of the machine architecture, we can make use of the scalar_storage_order Record attribute, as shown below:
1import
2 identifiers: all
3 from:
4 Tydl
5 Tydl.Data
6
7Simple2: Record
8 scalar_storage_order: most_significant_first
9 fields:
10 First: Field
11 type: 'Unsigned Integer'
12 bit_width: 16
13 Second: Field
14 type: 'Unsigned Integer'
15 bit_width: 16
If the scalar_storage_order attribute is not specified, then the machine’s native byte order will be used for efficiency.
Floating-Point Fields
In this example, let us consider yet another simple data structure: a record with two 32-bit floating-point fields, stored in little-endian format.
Field |
Type |
|---|---|
X |
|
Y |
The details of the IEEE 754 Single-Precision Floating-Point format can be found in the Wikipedia article:
When stored in byte-addressable memory on a little-endian machine, the data structure is layed out like this:
Byte Offset |
Content |
|---|---|
0 |
Least-Significant Byte of |
1 |
2nd Least-Significant Byte of |
2 |
2nd Most-Significant Byte of |
3 |
Most-Significant Byte of |
4 |
Least-Significant Byte of |
5 |
2nd Least-Significant Byte of |
6 |
2nd Most-Significant Byte of |
7 |
Most-Significant Byte of |
Once again, constructing a Tydl declaration with two Floating Point Values is straight-forward:
1import
2 identifiers: all
3 from:
4 Tydl
5 Tydl.Data
6
7Coordinates: Record
8 scalar_storage_order: least_significant_first
9 fields:
10 X: Field
11 type: 'Floating-Point Value'
12 bit_width: 32
13 Y: Field
14 type: 'Floating-Point Value'
15 bit_width: 32