1. Structure
Structure is a common design used in many languages to define users' custom types. They contains multiple values of different types, accessible by identifiers. Structures are similar to tuples, in terms of memory management (located in the stack). Unlike tuples, structures are named, and all their internal fields are named as well.
The complete grammar of structure definition is presented in the following code block. One can note the possibility to add templates to the definition of the structure. These templates will only be discussed in the chapter Templates, and are not of interest to us at the moment.
struct_type := 'struct' ('|' var_decl)* '->' identifier (templates)?
var_decl := ('mut'?) identifier ':' type ('=' expression)?
identifier := ('_')* [A-z] ([A-z0-9_])*
The fields of the structure are defined using the same syntax as the
declaration of function parameters, i.e. the same syntax as variable
declaration but with the keyword let
omitted. The following
source code presents a definition of a structure Point
with two
fields x
and y
of type i32
. The two fields of this
structure are immutable, and have no default values.
import std::io
struct
| x : i32
| y : i32
-> Point;
def main () {
let point = Point (1, 2); // initialize the value of the structure
println (point); // structures are printable
}
Results:
main::Point(1, 2)
It is possible to declare a structure with no fields. Note, however, that such structure has a size of 1 byte in memory.
Contribution this is a limitation observed in gcc, maybe this can be corrected ?
import std::io;
struct -> Unit;
def main () {
let x = Unit ();
println (x, " of size ", sizeof (x));
}
Results:
main::Unit() of size 1
1.1. Structure construction
The construction of a structure is made using the same syntax as a
function call, that is to say using its identifier and a list of
parameters inside parentheses and separated by comas. Like function
calls, structure can have default values assigneted to fields. The
value of these fields can be changed using the named expression
syntax, which is constructed with the arrow operator ->
. Field
without default value can also be constructed using the named
expression syntax. In that case, the order of field construction is
not important.
import std::io
struct
| x : i32 = 0
| y : i32
-> Point;
def main () {
let point = Point (y-> 12, x-> 98);
println (point);
let point2 = Point (1);
println (point2);
}
Results:
main::Point(98, 12)
main::Point(0, 1)
1.2. Field access
The fields of a structure are always public, and accessible using the
dot binary operator .
, where the left operand is a value whose
type is a structure, and the right operand is the identifier of the
field.
import std::io
struct
| x : i32
| y : i32
-> Point;
def main ()
throws &AssertError
{
let point = Point (1, 2);
assert (point.x == 1 && point.y == 2);
}
1.3. Structure mutability
The mutability of a field of a structure is defined in the structure
declaration. As with any variable declaration, the fields of a
structure are by default immutable. By adding the keyword mut
before the identifier of a field, the field becomes mutable. However,
the mutability is transitive in Ymir, meaning that a immutable value
of a struct type, cannot be modified even if its field are marked
mutable. Consequently, for a field to be really mutable, it must be
marked as such, and be a field of a mutable value.
import std::io
struct
| x : i32
| mut y : i32
-> Point;
def main () {
let mut p1 = Point (1, 2);
p1.y = 98; // y is mutable
// and p1 is mutable no problem
p1.x = 34; // x is not mutable, this won't compile
let p2 = Point (1, 2);
p2.y = 98; // p2 is not mutable, this won't compile
}
Errors:
Error : left operand of type i32 is immutable
--> main.yr:(13,4)
13 ┃ p1.x = 34; // x is not mutable, this won't compile
╋ ^
Error : left operand of type i32 is immutable
--> main.yr:(16,4)
16 ┃ p2.y = 98; // p2 is not mutable, this won't compile
╋ ^
ymir1: fatal error:
compilation terminated.
1.4. Memory borrowing of structure
By default structure data are located in the value that contains them, i.e. in the stack inside a variable, on the heap inside a slice, etc. They are copied by value, at assignement or function call. This copy is static, and does not require allocation, so it is allowed implicitely.
import std::io
struct
| mut x : i32
| mut y : i32
-> Point;
def main ()
throws &AssertError
{
let p = Point (1, 2);
let mut p2 = p; // make a copy of the structure
p2.y = 12;
assert (p.y == 2);
assert (p2.y == 12);
}
Structure may contain aliasable values, such as slice. In that case,
the copy is no longer allowed implicitely (if the structure is
mutable, and the field containing the aliasable value is also
mutable, and the element that will borrow the data is also
mutable). To resolve the problem, the keywords dcopy
, and alias
presented in Aliases and
References
can be used.
import std::io
struct
| mut y : [mut [mut i32]]
-> Point;
def main ()
throws &OutOfArray
{
let mut a = Point ([[1, 23, 3], [4, 5, 6]]);
let mut b = dcopy a;
let mut c = alias a;
b.y [0][0] = 9; // only change the value of 'b'
c.y [0][1] = 2; // change the value of 'a' and 'c'
println (a);
println (b);
println (c);
}
Results:
main::Point([[1, 2, 3], [4, 5, 6]])
main::Point([[9, 23, 3], [4, 5, 6]])
main::Point([[1, 2, 3], [4, 5, 6]])
It is impossible to make a simple copy of a structure with the keyword
copy
, the mutability level being set once and for all in the
structure definition. For example, if a structure S contains a field
whose type is mut [mut [i32]]
, every value of type S have a
field of type mut [mut [i32]]
. For that reason, by making a
first level copy, the mutability level would not be respected.
1.5. Packed and Union
This part only concerns advanced programming paradigms, and is close to the machine level. It is unlikely that you will ever need it, unless you try to optimize your code at a binary level.
1.5.1. Packed
The size of a structure is calculated by the compiler, which decides
the alignment of the different fields. This is why the size of a
structure containing an i64
and a c8
is 16 bytes, not 9
bytes. There is no guarantee about the size or the order of the fields
in the generated program. To force the compiler to remove the
optimized alignment, the special modifier packed
can be
used.
import std::io
struct @packed
| x : i64
| c : c8
-> Packed;
struct
| x : i64
| c : c8
-> Unpacked;
def main () {
println ("Size of packed : ", sizeof Packed);
println ("Size of unpacked : ", sizeof Unpacked);
}
Results:
Size of packed : 9
Size of unpacked : 16
1.5.2. Union
The union
special modifier , on the other hand, informs the
compiler that all fields in the structure must share the same memory
location. In the following example, the union
modifier is used
on a structure containing two fields. The largest field of the
structure is the field y
of type f64
. The size of this
field is 8 bytes, thus the structure has a size of 8 bytes as
well. All the fields are aligned at the beginning of the strucures,
meaning that the field x
, and y
shares the same address in
memory.
struct @union
| x : i32
| y : f64
-> Dummy;
The construction of a structure with union
modifier requires
only one argument. This argument must be passed as a named
expression with the arrow operator ->
.
import std::io
struct @union
| x : i32
| y : f32
-> Dummy;
def main ()
throws &AssertError
{
let x = Dummy (y-> 12.0f);
// Comparison of pointer is only possible on pointer of the same type
// Any pointer can be casted into a pointer of &void (the contrary is not possible)
// is operator, checks if two pointer are equals
assert (cast!(&void) (&(x.x)) is cast!(&void) (&(x.y)));
// The value of x depends on the value of y
assert (x.x == 1094713344);
assert (x.y == 12.0f);
}
1.6. Structure specific attributes
Structures have type specific attributes, as any types, accessible with
the double colon binary operator ::
. The table below presents
these specific attributes. These attributes are accessible using a
type of struct, and not a value. A example, under the table presents
usage of struct specific attributes.
Name | Meaning |
---|---|
init |
The initial value of the type |
typeid |
The name of the type stored in a value of type [c32] |
typeinfo |
A structure of type TypeInfo, containing information about the type |
All the information about TypeInfo are presented in chapter Dynamic types.
mod main;
import std::io;
struct
| x : i32
| y : i32 = 9
-> Point;
def main ()
throws &AssertError
{
let x = Point::init;
// the structure is declared in the main module, thus its name is main::Point
assert (Point::typeid == "main::Point");
assert (x.x == i32::init && x.y == 9);
}