Next: Dealing with Alternatives, Previous: Functions in Structs, Up: Structuring Data [Contents][Index]
At this point you may be thinking something on the line of “hey, since variables and functions are also members of the struct, I should be able to access them the same way than fields, right?”.
So you will want to do:
(poke) var p = Packet 12#B (poke) p.real_size (poke) p.corrected_crc
But sorry, this won’t work.
To understand why, think about the struct building process we sketched above. The mapper and constructor functions are derived/compiled from the struct type. You can imagine them to have prototypes like:
Packet_mapper (IOspace, offset) -> Packet value Packet_constructor (template) -> Packet value
You can also picture the fields, variables and functions in the struct type specification as being defined inside the bodies of Packet_mapper and Packet_constructor, as their contents get mapped/constructed. For example, let’s see what the mapper does:
Packet_mapper: . Map a byte, put it in a local `magic'. . Map a byte, put it in a local `size'. . Calculate the real size, put it in a local `real_size'. . Map an array of real_size bytes, put it in a local `payload'. . Map an array of real_size bytes, put it in a local `control'. . Compile a function, put it in a local `corrected_crc'. . map a byte, call the function in the local `corrected_crc', complain if the values are not the same, otherwise put the mapped byte in a local `crc'. . Build a struct value with the values from the locals `magic', `size', `payload', `control' and `crc', and return it.
The pseudo-code for the constructor would be almost identical. Just replace "map a byte" with “construct a byte”.
So you see, both the values for the mapped fields and the values for the variables and functions defined inside the struct type end as locals of the mapping process, but only the values of the fields are actually put in the struct value that is returned in the last step.
This is where methods come in the picture. A method looks very similar to a function, but it is not quite the same thing. Let me show you an example:
load supercrc; type Packet = struct { byte magic = 0xab; byte size; var real_size = (size == 0xff ? 0 : size); byte[real_size] payload; byte[real_size] control; fun corrected_crc = int: { try return calculate_crc (payload, control); catch if E_div_by_zero { return 0; } } int crc = corrected_crc; method c_crc = int: { return corrected_crc; } };
We have added a method c_crc
to our Packet struct type, that
just returns the corrected superCRC (patented, TM) of a packet. This
can be invoked using dot-notation, once a Packet value is
mapped/constructed:
(poke) var p = Packet 12#B (poke) p.c_crc 0xdeadbeef
Now, the important bit here is that the method returns the corrected
crc of a Packet
. That’s it, it actually operates on a
Packet value. This Packet value gets implicitly passed as an argument
whenever a method is invoked.
We can visualize this with the following “pseudo Poke”:
method c_crc = (Packet SELF) int: { return SELF.corrected_crc; }
Fortunately, poke takes care to recognize when you are referring to fields of this implicit struct value, and does The Right Thing(TM) for you. This includes calling other methods:
method foo = void: { ... } method bar = void: { [...] foo; }
The corresponding “pseudo-poke” being:
method bar = (Packet SELF) void: { [...] SELF.foo (); }
It is also possible to define methods that modify the contents of struct fields, no problem:
var packet_special = 0xff; type Packet = struct { byte magic = 0xab; byte size; [...] method set_size = (byte s) void: { if (s == 0) size = packet_special; else size = s; } };
This is what is commonly known as a setter. Note, incidentally,
how a method can also use regular variables. The Poke compiler knows
when to generate a store in a normal variable such as
packet_special
, and when to generate a set to a SELF
field.
Given the different nature of the variables, functions and methods, there are a couple of restrictions:
This will be rejected by the compiler:
type Foo = struct { int field; fun wrong = void: { field = 10; } };
Remember the construction/mapping process. When a function accesses a
field of the struct type like in the example above, it is not doing
one of these pseudo SELF.field = 10
. Instead, it is simply
updating the value of the local created in this step in Foo_mapper:
Foo_mapper: . Map an int, put it in a local `field'. . [...]
Setting that local would impact the mapping of the subsequent fields
if they refer to field
(for example, in their constraint
expression) but it wouldn’t actually alter the value of the field
field
in the struct value that is created and returned from the
mapper!
This is very confusing, so we just disallow this with a compiler error “invalid assignment to struct field”, for your own sanity 8-)
How could they be? The field constraint expressions, the initialization expressions of variables, and the functions defined in struct types are all executed as part of the mapper/constructor and, at that time, there is no struct value yet to pass to the method.
If you try to do this, the compiler will greet you with an “invalid reference to struct method” message.
Next: Dealing with Alternatives, Previous: Functions in Structs, Up: Structuring Data [Contents][Index]