1 | Proposal for virtual functionality |
---|
2 | |
---|
3 | There are two types of virtual inheritance in this proposal, relaxed |
---|
4 | (implicit) and strict (explicit). Relaxed is the simpler case that uses the |
---|
5 | existing trait system with the addition of trait references and vtables. |
---|
6 | Strict adds some constraints and requires some additional notation but allows |
---|
7 | for down-casting. |
---|
8 | |
---|
9 | Relaxed Virtual Inheritance: |
---|
10 | |
---|
11 | Imagine the following code : |
---|
12 | |
---|
13 | trait drawable(otype T) { |
---|
14 | void draw(T* ); |
---|
15 | }; |
---|
16 | |
---|
17 | struct text { |
---|
18 | char* text; |
---|
19 | }; |
---|
20 | |
---|
21 | void draw(text*); |
---|
22 | |
---|
23 | struct line{ |
---|
24 | vec2 start; |
---|
25 | vec2 end; |
---|
26 | }; |
---|
27 | |
---|
28 | void draw(line*); |
---|
29 | |
---|
30 | While all the members of this simple UI support drawing, creating a UI that |
---|
31 | easily supports both these UI requires some tedious boiler-plate code: |
---|
32 | |
---|
33 | enum type_t { text, line }; |
---|
34 | |
---|
35 | struct widget { |
---|
36 | type_t type; |
---|
37 | union { |
---|
38 | text t; |
---|
39 | line l; |
---|
40 | }; |
---|
41 | }; |
---|
42 | |
---|
43 | void draw(widget* w) { |
---|
44 | switch(w->type) { |
---|
45 | case text : draw(&w->text); break; |
---|
46 | case line : draw(&w->line); break; |
---|
47 | default : handle_error(); break; |
---|
48 | } |
---|
49 | } |
---|
50 | |
---|
51 | While this code will work as implemented, adding any new widgets or any new |
---|
52 | widget behaviors requires changing existing code to add the desired |
---|
53 | functionality. To ease this maintenance effort required CFA introduces the |
---|
54 | concept of trait references. |
---|
55 | |
---|
56 | Using trait references to implement the above gives the following : |
---|
57 | |
---|
58 | trait drawable objects[10]; |
---|
59 | fill_objects(objects); |
---|
60 | |
---|
61 | while(running) { |
---|
62 | for(drawable object : objects) { |
---|
63 | draw(object); |
---|
64 | } |
---|
65 | } |
---|
66 | |
---|
67 | The keyword trait is optional (by the same rules as the struct keyword). This |
---|
68 | is not currently supported in CFA and the lookup is not possible to implement |
---|
69 | statically. Therefore we need to add a new feature to handle having dynamic |
---|
70 | lookups like this. |
---|
71 | |
---|
72 | What we really want to do is express the fact that calling draw() on a trait |
---|
73 | reference should find the underlying type of the given parameter and find how |
---|
74 | it implements the routine, as in the example with the enumeration and union. |
---|
75 | |
---|
76 | For instance specifying that the drawable trait reference looks up the type |
---|
77 | of the first argument to find the implementation would be : |
---|
78 | |
---|
79 | trait drawable(otype T) { |
---|
80 | void draw(virtual T* ); |
---|
81 | }; |
---|
82 | |
---|
83 | This could be implied in simple cases like this one (single parameter on the |
---|
84 | trait and single generic parameter on the function). In more complex cases it |
---|
85 | would have to be explicitly given, or a strong convention would have to be |
---|
86 | enforced (e.g. implementation of trait functions is always drawn from the |
---|
87 | first polymorphic parameter). |
---|
88 | |
---|
89 | Instances of a trait are created by wrapping an existing instance of a type |
---|
90 | that implements that trait. This wrapper includes all the function pointers |
---|
91 | and other values required to preform the dynamic look-up. These are chosen by |
---|
92 | the normal look-up rules at the point of abstraction. |
---|
93 | |
---|
94 | One of the limitations of this design is that it does not support double |
---|
95 | dispatching, which concretely means traits cannot have routines with more than |
---|
96 | one virtual parameter. The program must have a single table to look up the |
---|
97 | function on. Using trait references with traits with more than one parameter |
---|
98 | is also restricted, initially forbidden, see extension. |
---|
99 | |
---|
100 | Ownership of the underlying structure is also a bit of a trick. Considering |
---|
101 | the use cases for trait object, it is probably best to have the underlying |
---|
102 | object be heap allocated and owned by the trait object. |
---|
103 | |
---|
104 | Extension: Multi-parameter Virtual Traits: |
---|
105 | |
---|
106 | This implementation can be extended to traits with multiple parameters if |
---|
107 | one is called out as being the virtual trait. For example : |
---|
108 | |
---|
109 | trait iterator(otype T, dtype Item) { |
---|
110 | Maybe(Item) next(virtual T *); |
---|
111 | } |
---|
112 | |
---|
113 | iterator(int) generators[10]; |
---|
114 | |
---|
115 | Which creates a collection of iterators that produce integers, regardless of |
---|
116 | how those iterators are implemented. This may require a note that this trait |
---|
117 | is virtual on T and not Item, but noting it on the functions may be enough. |
---|
118 | |
---|
119 | |
---|
120 | Strict Virtual Inheritance: |
---|
121 | |
---|
122 | One powerful feature relaxed virtual does not support is the idea of down |
---|
123 | casting. Once something has been converted into a trait reference there is |
---|
124 | very little we can do to recover and of the type information, only the trait's |
---|
125 | required function implementations are kept. |
---|
126 | |
---|
127 | To allow down casting strict virtual requires that all traits and structures |
---|
128 | involved be organized into a tree. Each trait or struct must have a unique |
---|
129 | position on this tree (no multiple inheritance). |
---|
130 | |
---|
131 | This is declared as follows : |
---|
132 | |
---|
133 | trait error(otype T) virtual { |
---|
134 | const char * msg(T *); |
---|
135 | } |
---|
136 | |
---|
137 | trait io_error(otype T) virtual error { |
---|
138 | FILE * src(T *); |
---|
139 | } |
---|
140 | |
---|
141 | struct eof_error virtual io_error { |
---|
142 | FILE * fd; |
---|
143 | }; |
---|
144 | |
---|
145 | So the trait error is the head of a new tree and io_error is a child of it. |
---|
146 | |
---|
147 | Also the parent trait is implicitly part of the assertions of the children, |
---|
148 | so all children implement the same operations as the parent. By the unique |
---|
149 | path down the tree, we can also uniquely order them so that a prefix of a |
---|
150 | child's vtable has the same format as its parent's. |
---|
151 | |
---|
152 | This gives us an important extra feature, runtime checking of the parent-child |
---|
153 | relationship with virtual cast, where a pointer (and maybe a reference) to |
---|
154 | a virtual type can be cast to another virtual cast. However the cast is |
---|
155 | dynamicly check and only occurs if the underlying type is a child of the type |
---|
156 | being cast to. Otherwise null is returned. |
---|
157 | |
---|
158 | (virtual TYPE)EXPR |
---|
159 | |
---|
160 | As an extention, the TYPE may be ommitted if it can be determained from |
---|
161 | context, for instance if the cast occurs on the right hand side of an |
---|
162 | assignment. |
---|
163 | |
---|
164 | Function look-up follows the same rules as relaxed (behavioural) inheritance. |
---|
165 | Traits can be upcast and down cast without losing information unless the |
---|
166 | trait is cast down to a structure. Here there are two options. |
---|
167 | |
---|
168 | Abstraction Time Binding: The more efficient and consistant with other parts |
---|
169 | of CFA. Only the trait types use dynamic look-up, if converveted back into a |
---|
170 | structure the normal static look-up rules find the function at compile time. |
---|
171 | Casting down to a structure type can then result in the loss of a set of |
---|
172 | bindings. |
---|
173 | Construction Time Binding: For more consistant handling of the virtual |
---|
174 | structs, they are always considered wrapped. Functions are bound to the |
---|
175 | instance the moment it is constructed and remain unchanged throughout its |
---|
176 | lifetime, so down casting does not lose information. |
---|
177 | |
---|
178 | (We will have to decide between one of these two.) |
---|
179 | |
---|
180 | Extension: Multiple Parents |
---|
181 | |
---|
182 | Although each trait/struct must have a unique position on each tree, it could |
---|
183 | have positions on multiple trees. All this requires is the ability to give |
---|
184 | multiple parents, as here : |
---|
185 | |
---|
186 | trait region(otype T) virtual drawable, collider; |
---|
187 | |
---|
188 | The restriction being, the parents must come from different trees. This |
---|
189 | object (and all of its children) can be cast to either tree. This is handled |
---|
190 | by generating a separate vtable for each tree the structure is in. |
---|
191 | |
---|
192 | Extension: Multi-parameter Strict Virtual |
---|
193 | |
---|
194 | If a trait has multiple parameters then one must be called out to be the one |
---|
195 | we generate separate vtables for, as in : |
---|
196 | |
---|
197 | trait example(otype T, otype U) virtual(T) ... |
---|
198 | |
---|
199 | This can generate a separate vtable for each U for which all the T+U |
---|
200 | implementations are provided. These are then separate nodes in the tree (or |
---|
201 | the root of different trees) as if each was created individually. Providing a |
---|
202 | single unique instance of these nodes would be the most difficult aspect of |
---|
203 | this extension, possibly intractable, though with sufficient hoisting and |
---|
204 | link-once duplication it may be possible. |
---|
205 | |
---|
206 | Example: |
---|
207 | |
---|
208 | trait argument(otype T) virtual { |
---|
209 | char short_name(virtual T *); |
---|
210 | bool is_set(virtual T *); |
---|
211 | }; |
---|
212 | |
---|
213 | trait value_argument(otype T, otype U) virtual(T) argument { |
---|
214 | U get_value(virtual T *); |
---|
215 | }; |
---|
216 | |
---|
217 | Extension: Structural Inheritance |
---|
218 | |
---|
219 | Currently traits must be the internal nodes and structs the leaf nodes. |
---|
220 | Structs could be made internal nodes as well, in which case the child structs |
---|
221 | would likely structurally inherit the fields of their parents. |
---|
222 | |
---|
223 | |
---|
224 | Storing the Virtual Lookup Table (vtable): |
---|
225 | |
---|
226 | We have so far been silent on how the vtable is created, stored and accessed. |
---|
227 | The vtables for the two types might be handled slightly differently and then |
---|
228 | there is also the hierarchy data for virtual casts. |
---|
229 | |
---|
230 | The hierarchy data is simple conceptually. A single (exactly one copy) pointer |
---|
231 | for each type can act as the identity for it. The value of the pointer is |
---|
232 | its parent type, with the root pointer being NULL. Additional meta-data |
---|
233 | can accompany the parent pointer, such as a string name or the vtable fields. |
---|
234 | |
---|
235 | They types of each vtable can be constructed from the definitions of the |
---|
236 | traits (or internal nodes). The stand alone/base vtable is the same for both |
---|
237 | kinds of inheritance. It may be argumented differently however (include parent |
---|
238 | /this pointer in hierachal inheritance). |
---|
239 | |
---|
240 | Creation of the actual vtable is tricky. For classical single implementation |
---|
241 | semantics we would assemble the functions and create one vtable at compile |
---|
242 | time. However, not only does this not give CFA-like behaviour, it is |
---|
243 | impossible generally because types can satify assertions in different ways at |
---|
244 | different times and stop satifying them. A special set of harder rules could |
---|
245 | be used, instead we have decided to try creating multiple vtables for each |
---|
246 | type. The different vtables will all implement the same type but not always |
---|
247 | in the same way. |
---|
248 | |
---|
249 | Storage has some issues from creation. If the contents of every vtable could |
---|
250 | be determained at compile time they could all be created and stored |
---|
251 | statically. However since thunks can be constructed on the stack and become |
---|
252 | the best match, that isn't always possible. Those will have to be stored in |
---|
253 | dynamic memory. Which means that all vtables must be stored dynamically or |
---|
254 | there must be a way to determain which ones to free when the trait object is |
---|
255 | destroyed. |
---|
256 | |
---|
257 | Access has two main options: |
---|
258 | |
---|
259 | The first is through the use of fat pointers, or a tuple of pointers. When the |
---|
260 | object is converted to a trait reference, the pointers to its vtables are |
---|
261 | stored along side it. |
---|
262 | |
---|
263 | This allows for compatibility with existing structures (such as those imported |
---|
264 | from C) and is the default storage method unless a different one is given. |
---|
265 | |
---|
266 | The other is by inlining the vtable pointer as "intrusive vtables". This adds |
---|
267 | a field to the structure to the vtable. The trait reference then has a single |
---|
268 | pointer to this field, the vtable includes an offset to find the beginning of |
---|
269 | the structure again. |
---|
270 | |
---|
271 | This is used if you specify a vtable field in the structure. If given in the |
---|
272 | trait the vtable pointer in the trait reference can then become a single |
---|
273 | pointer to the vtable field and use that to recover the original object |
---|
274 | pointer as well as retrieve all operations. |
---|
275 | |
---|
276 | trait drawable(otype T) { |
---|
277 | vtable drawable; |
---|
278 | }; |
---|
279 | |
---|
280 | struct line { |
---|
281 | vtable drawable; |
---|
282 | vec2 start; |
---|
283 | vec2 end; |
---|
284 | }; |
---|
285 | |
---|
286 | This inline code allows trait references to be converted to plain pointers |
---|
287 | (although they still must be called specially). The vtable field may just be |
---|
288 | an opaque block of memory or it may allow user access to the vtable. If so |
---|
289 | then there should be some way to retrieve the type of the vtable, which will be |
---|
290 | autogenerated and often unique. |
---|
291 | |
---|
292 | It may be worth looking into a way to force the vtable pointer to be in a |
---|
293 | particular location, which would save the storage to store the offset and |
---|
294 | maybe the offset operation itself (offset = 0). However it may not be worth |
---|
295 | introducing a new language feature for. |
---|
296 | As of writing, exceptions actually use this system. |
---|
297 | |
---|
298 | |
---|
299 | Keyword Usage: |
---|
300 | |
---|
301 | It may be desirable to add fewer new keywords than discussed in this proposal. |
---|
302 | It is possible that "virtual" could replace both "vtable" above with |
---|
303 | unambiguous contextual meaning. However, for purposes of clarity in the design |
---|
304 | discussion it is beneficial to keep the keywords for separate concepts distinct. |
---|
305 | |
---|
306 | |
---|
307 | Trait References and Operations: |
---|
308 | |
---|
309 | sizeof(drawable) will return the size of the trait object itself. However : |
---|
310 | |
---|
311 | line a_line; |
---|
312 | drawable widget = a_line; |
---|
313 | sizeof(widget); |
---|
314 | |
---|
315 | Will instead return the sizeof the underlying object, although the trait must |
---|
316 | require that its implementation is sized for there to be a meaningful value |
---|
317 | to return. You may also get the size of the trait reference with |
---|
318 | |
---|
319 | sizeof(&widget); |
---|
320 | |
---|
321 | Calling free on a trait reference will free the memory for the object. It will |
---|
322 | leave the vtables alone, as those are (always?) statically allocated. |
---|
323 | |
---|
324 | |
---|
325 | Special Traits: |
---|
326 | |
---|
327 | trait is_virtual_parent(dtype parent, dtype child) { ... }; |
---|
328 | |
---|
329 | There are others but I believe this one to be the most important. The trait |
---|
330 | holds if the parent type is a strict virtual ancestor (any number of levels) |
---|
331 | of child. It will have to exist at least internally to check for upcasts and |
---|
332 | it can also be used to optimize virtual casts into upcasts. Or a null value or |
---|
333 | error if the cast would never succeed. Exporting it to a special trait allows |
---|
334 | users to express that requirement in their own polymorphic code. |
---|
335 | |
---|
336 | |
---|
337 | Implementation: |
---|
338 | |
---|
339 | Before we can generate any of the nessasary code, the compiler has to get some |
---|
340 | additional information about the code that it currently does not collect. |
---|
341 | |
---|
342 | First it should establish all child->parent links so that it may travel up the |
---|
343 | hierarchy to grab the nessasary information, and create the actual parent |
---|
344 | pointers in the strict virtual tables. It should also maintain the connections |
---|
345 | between the virtual type (structure or trait), the vtable type and the vtable |
---|
346 | instance (or default instance for relaxed virtual if multiple are allowed). To |
---|
347 | this end a sub-node should be created with the nessasary pointers. Traits and |
---|
348 | structs with virtual can create an instance and store all the nessasary data. |
---|
349 | |
---|
350 | With the hierarchy in place it can generate the vtable type for each type, |
---|
351 | it will generally have a function pointer field for each type assertion in |
---|
352 | some consistant order. Strict virtual will also have a pointer to the parent's |
---|
353 | vtable and intrusive vtables will also have the offset to recover the original |
---|
354 | pointer. Sized types will also carry the size. |
---|
355 | |
---|
356 | Wheither the vtable is intrusive or not should also be save so that the trait |
---|
357 | object/reference/pointer knows if it has to store 1 or 2 pointers. A wrapper |
---|
358 | function will have to be generated for each type assertion so that they may |
---|
359 | be called on the trait type, these can probably be inlined. |
---|
360 | |
---|
361 | The virtual parameter will also have to be marked (implicately or explicately) |
---|
362 | until code generation so that the wrapper functions know where to go to get |
---|
363 | the vtable for the function look up. That could probably be added as a |
---|
364 | storageclass, although one that is only valid on type assertions. |
---|
365 | |
---|
366 | The generated vtable will than have to have a vtable instance created and |
---|
367 | filled with all the approprate values. Stricter matching may have to be used |
---|
368 | to ensure that the functions used are stable. It will also have to use |
---|
369 | ".gnu.linkonce" or equilant to ensure only one copy exists in the final code |
---|
370 | base. |
---|