Sign in Join
Wiki copytemporary

default properties: what changed

Revision 2 to 3. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11Default properties allow "hoisting" or exposing of object members into the caller name space to facilitate wrapper types and (possibly) delegation patterns. They could be seen as "with" statements which encompass an entire structures namespace.
22
33This feature is under development and everything is subject to change.
44
55## Download Development Branch
66
77<https://github.com/genericptr/freepascal/tree/defaultprops>
88
99## Supports
1010
1111- records, objects, classes.
1212- arithmetic, compare, binary, unary, in overloads. (https://www.freepascal.org/docs-html/ref/refch15.html)
1313- visibility sections.
1414- array indexing with \[\]
1515- if, while, repeat, case, for..do statements
1616
1717## Hoisting Members
1818
1919By making "m_obj" a default property we can omit m_obj. to subscript into the members. This is like wrapping every instance of "m_obj" in a "with" statement. This makes it possible to bring members from other structures into the current structure without prefixing "m_obj." before accessing fields.
2020
2121``` pascal
2222 type
2323 THelper = record
2424 num: integer;
2525 end;
2626
2727 type
2828 TWrapper = record
2929 m_obj: THelper;
3030 property helper: THelper read m_obj; default;
3131 procedure Inc;
3232 end;
3333
3434 procedure TWrapper.Inc;
3535 begin
3636 num += 1;
3737 end;
3838
3939 var
4040 wrapper: TWrapper;
4141 begin
4242 wrapper.num := 100;
4343 wrapper.Inc;
4444 writeln(wrapper.num);
4545 end.
4646```
4747
4848## Precedence Rules
4949
5050For default properties which are structures (objects, records, classes) it's possible to have method overloads in both the base structure and the default property (or unit scope even). For this reason overload resolution must follow a predicable pattern.
5151
5252Precedence order for overloads is as follows: base, default (last to first).
5353
5454``` pascal
5555 type
5656 THelper = class
5757 num: integer;
5858 procedure DoThis; overload;
5959 procedure DoThis (param: integer); overload;
6060 procedure DoThis (param: string); overload;
6161 end;
6262
6363 procedure THelper.DoThis;
6464 begin
6565 writeln('THelper.DoThis:',num);
6666 end;
6767
6868 procedure THelper.DoThis (param: integer);
6969 begin
7070 writeln('THelper.DoThis:',param,':',num);
7171 end;
7272
7373 procedure THelper.DoThis (param: string);
7474 begin
7575 writeln('THelper.DoThis:',param,':',num);
7676 end;
7777
7878 type
7979 TParent = class
8080 m_helper: THelper;
8181 property helper: THelper read m_helper; default;
8282 procedure DoThis (param:single);overload;
8383 end;
8484
8585 procedure TParent.DoThis (param:single);
8686 begin
8787 writeln('TParent.DoThis:',param:1:1);
8888 end;
8989
9090 type
9191 TMyObject = class (TParent)
9292 procedure Call;
9393 procedure DoThis;overload;
9494 end;
9595
9696 procedure TMyObject.DoThis;
9797 begin
9898 writeln('TMyObject.DoThis');
9999 end;
100100
101101 procedure TMyObject.Call;
102102 begin
103103 DoThis(100); // THelper.DoThis
104104 DoThis(10.5); // TParent.DoThis
105105 DoThis('hello'); // THelper.DoThis
106106 DoThis; // TMyObject.DoThis
107107 end;
108108
109109 var
110110 obj: TMyObject;
111111 begin
112112 obj := TMyObject.Create;
113113 obj.m_helper := THelper.Create;
114114 obj.Call;
115115 end.
116116```
117117
118118Operator overloads are possible also. Precedence can get complicated considering it's possible to have overloads in the current unit for the default type as well as overloads in the structure itself.
119119
120120``` pascal
121121 operator + (l : integer; r : ansistring): integer;
122122 begin
123123 result := l + StrToInt(r);
124124 end;
125125
126126 type
127127 TWrapper = record
128128 m_value: integer;
129129 property value: integer read m_value write m_value; default;
130130 end;
131131
132132 var
133133 wrapper: TWrapper;
134134 begin
135135 wrapper += 1; // default property takes precedence
136136 wrapper += '128'; // unit + operator takes precedence because default property type (integer) doesn't support string overloads
137137 writeln(wrapper.value); // 129
138138 if wrapper = 129 then // = operator works also
139139 writeln('got 129');
140140 end.
141141```
142142
143143Type helpers behave as normal on the default type.
144144
145145``` pascal
146146 type
147147 TIntegerHelper = type helper for integer
148148 function Str: string;
149149 end;
150150
151151 function TIntegerHelper.Str: string;
152152 begin
153153 result := 'string->'+IntToStr(self);
154154 end;
155155
156156 type
157157 TWrapper = record
158158 m_value: integer;
159159 property value: integer read m_value write m_value; default;
160160 end;
161161
162162 var
163163 wrapper: TWrapper;
164164 begin
165165 wrapper := 100;
166166 writeln(wrapper.str);
167167 end.
168168```
169169
170170## Passing
171171
172172Passing a structure with default properties follows normal move semantics. For example, if there is a record with a default property of the type "integer" passing the record will not copy the integer value but rather the entire record will be passed.
173173
174174``` pascal
175175 type
176176 TIntRec = record
177177 m_int: integer;
178178 property value: integer read m_obj; default;
179179 end;
180180
181181 procedure Print (int: TIntRec); overload;
182182 begin
183183 writeln(int.value);
184184 end;
185185
186186 procedure Print (int: integer); overload;
187187 begin
188188 writeln(int);
189189 end;
190190
191191 var
192192 int: TIntRec;
193193 begin
194194 int := 100;
195195 Print(int); // Print(TIntRec) is called
196196 writeln(int) // ERROR: can't write this type (TIntRec)
197197 end.
198198```
199199
200200## Assignment Rules
201201
202202Default properties introduce an ambiguity with assignments which is resolved by type.
203203
204204``` pascal
205205 type
206206 TWrapper = class
207207 m_value: integer;
208208 m_string: string;
209209 property value: integer read m_value write m_value; default;
210210 end;
211211
212212 var
213213 wrapper: TWrapper;
214214 begin
215215 wrapper := TWrapper.Create; // TWrapper is same type so assignment is as normal
216216 wrapper := 100; // integer matches "value"
217217 writeln(wrapper.value);
218218 end.
219219```
220220
221221## Multiple Defaults
222222
223223Currently for development default properties are "multidimensional", meaning you can declare more than one per type and overloads will be resolved according to precedence.
224224
225225They have been developed this way from the outset because the nature of the search is inherently multidimensional, i.e base object, default property 1, default property 2 etc... so I wanted to keep the option available while the concept is being explored.
226226
227227This is of course a highly contested idea given the capacity to introduce difficult to understand namespace collisions.
228228
229229## Hiding Fields
230230
231231If multiple defaults are allowed then it would be possible to hide fields from other default properties and cause some potential bugs.
232232
233233``` pascal
234234 type
235235 THelper = record
236236 data: integer;
237237 end;
238238
239239 type
240240 TWrapper = record
241241 m_objA: THelper;
242242 m_objB: THelper;
243243 property helperA: THelper read m_objA; default;
244244 property helperB: THelper read m_objB; default;
245245 end;
246246
247247 var
248248 wrapper: TWrapper;
249249 begin
250250 wrapper.data := 1;
251251 writeln('helperA:',wrapper.helperA.data); // 0
252252 writeln('helperB:',wrapper.helperB.data); // 1
253253 end.
254254```
255255
256256## Examples Usages
257257
258258### Nullable types
259259
260260``` pascal
261261 type
262262 generic TNullable<T>= record
263263 private
264264 FValue : T;
265265 public
266266 isAssigned : boolean;
267267 function IsNull: boolean;
268268 procedure SetValue (newValue: T);
269269 Property Value : T read FValue write SetValue; default;
270270 class operator Initialize(var a: TNullable);
271271 end;
272272
273273 procedure TNullable.SetValue (newValue: T);
274274 begin
275275 FValue := newValue;
276276 isAssigned := true;
277277 end;
278278
279279 function TNullable.IsNull: boolean;
280280 begin
281281 result := not isAssigned;
282282 end;
283283
284284 class operator TNullable.Initialize(var a: TNullable);
285285 begin
286286 a.isAssigned := Default(T);
287287 end;
288288
289289 type
290290 TBoolean = specialize TNullable<boolean>;
291291 Var
292292 bool: TBoolean;
293293 begin
294294 bool := true;
295295 if bool then
296296 writeln('bool is true');
297297 end.
298298```
299299
300300### Auto managed classes using management operators
301301
302302``` pascal
303303 type
304304 generic TAuto<T> = record
305305 private
306306 m_object: T;
307307 class operator Initialize(var a: TAuto);
308308 class operator Finalize(var a: TAuto);
309309 public
310310 property obj: T read m_object; default;
311311 end;
312312
313313 type
314314 TStringList = specialize TFPGList<String>;
315315 TStringListAuto = specialize TAuto<TStringList>;
316316
317317 class operator TAuto.Initialize(var a: TAuto);
318318 begin
319319 a.m_object := T.Create;
320320 end;
321321
322322 class operator TAuto.Finalize(var a: TAuto);
323323 begin
324324 a.m_object.Free;
325325 end;
326326
327327 var
328328 list: TStringListAuto;
329329 str: string;
330330 begin
331331 list.Add('foo');
332332 list.Add('bar');
333333 for str in list do
334334 writeln(str);
335335 end.
336336```
337337
338338### Array indexing
339339
340340Because it is technically possible arrays are exposed using default properties. This however does conflict with the existing \[\] indexer properties so this may be a problem.
341341
342342As it stands however default properties on array types are an interesting method to make array wrappers.
343343
344344``` pascal
345345 type
346346 TIntArray = array of integer;
347347 TWrapper = record
348348 m_value: TIntArray;
349349 property value: TIntArray read m_value write m_value; default;
350350 end;
351351
352352 var
353353 wrapper: TWrapper;
354354 i: integer;
355355 begin
356356 wrapper := TIntArray.Create(100,200,300); // NOTE: we can't use array constructors [] due to bug in compiler
357357 wrapper[0] += 50;
358358 wrapper[1] += 1;
359359 for i in wrapper do
360360 writeln(i);
361361 end.
362362```
363363
364364### Delegation
365365
366366If delegate properties are allowed be multidimensional in the final version they can be used for delegation patterns that would otherwise only be possible with multiple inheritance (in languages such as C++, Java).
367367
368368``` pascal
369369
370370 type
371371 TStats = record
372372 hp: integer;
373373 exp: integer;
374374 procedure InitStats;
375375 end;
376376
377377 // Base Module:
378378
379379 type
380380 TEntity = class;
381381 TBaseHandler = class
382382 entity: TEntity;
383383 constructor Create (inEntity: TEntity);
384384 procedure Clear;
385385 end;
386386
387387 // Physics Module:
388388
389389 TPhysicsHandler = class (TBaseHandler)
390390 pos: TVec3;
391391 acc: TVec3;
392392 vel: TVec3;
393393 procedure Integrate; virtual;
394394 end;
395395
396396 // Renderer Module:
397397
398398 TRendererHandler = class (TBaseHandler)
399399 procedure Render; virtual;
400400 end;
401401
402402 // Entity:
403403
404404 TEntity = class
405405 private
406406 m_physics: TPhysicsHandler;
407407 m_renderer: TRendererHandler;
408408 m_stats: TStats;
409409 public
410410 property physics: TPhysicsHandler read m_physics; default;
411411 property renderer: TRendererHandler read m_renderer; default;
412412 property stats: TStats read m_stats; default;
413413 public
414414 procedure Update;
415415 end;
416416
417417 // Implemention:
418418
419419 type
420420 TMonster = class (TEntity)
421421 public
422422 procedure AfterConstruction; override;
423423 end;
424424
425425 type
426426 TMonsterRenderer = class (TRendererHandler)
427427 procedure Render; override;
428428 end;
429429
430430 procedure TStats.InitStats;
431431 begin
432432 hp := 100;
433433 exp := 0;
434434 end;
435435
436436 procedure TBaseHandler.Clear;
437437 begin
438438 writeln(classname,' clear');
439439 end;
440440
441441 constructor TBaseHandler.Create (inEntity: TEntity);
442442 begin
443443 entity := inEntity;
444444 end;
445445
446446 procedure TPhysicsHandler.Integrate;
447447 begin
448448 pos.x += vel.x;
449449 pos.y += vel.y;
450450 pos.z += vel.z;
451451 end;
452452
453453 procedure TRendererHandler.Render;
454454 begin
455455 end;
456456 RSS
457457 procedure TEntity.Update;
458458 begin
459459 Integrate;
460460 Render;
461461 end;
462462
463463 procedure TMonster.AfterConstruction;
464464 begin
465465 m_physics := TPhysicsHandler.Create(self);
466466 m_renderer := TMonsterRenderer.Create(self);
467467
468468 InitStats;
469469
470470 vel.x := 80;
471471 vel.y := 20;
472472 vel.z := 0;
473473 end;
474474
475475 procedure TMonsterRenderer.Render;
476476 begin
477477 writeln('render monster at ', entity.pos.str, ' hp:', entity.hp);
478478 end;
479479
480480 var
481481 entity: TMonster;
482482 i: integer;
483483 begin
484484 entity := TMonster.Create;
485485 entity.Clear;
486486 for i := 0 to 2 do
487487 entity.Update;
488488 end.
489489```
490490
491491### Default "Implements" Properties
492492
493493"implements" properties are more complete by exposing their namespace using "default". If multiple defaults are allowed one could imagine this being a possible method to implement multiple inheritance in Pascal.
494494
495495``` pascal
496496 type
497497 IWrapper = interface ['IWrapper']
498498 procedure DoThis;
499499 end;
500500
501501 type
502502 TWrapper_Handler = class (IWrapper)
503503 procedure DoThis;
504504 end;
505505
506506 procedure TWrapper_Handler.DoThis;
507507 begin
508508 writeln('TWrapper_Handler.DoThis');
509509 end;
510510
511511 type
512512 TWrapper = class (IWrapper)
513513 m_wrapper: TWrapper_Handler;
514514 property handler: TWrapper_Handler read m_wrapper implements IWrapper; default;
515515 procedure AfterConstruction; override;
516516 end;
517517
518518 procedure TWrapper.AfterConstruction;
519519 begin
520520 m_wrapper := TWrapper_Handler.Create;
521521 end;
522522
523523 procedure HandleWrapper (wrapper: IWrapper);
524524 begin
525525 wrapper.DoThis;
526526 end;
527527
528528 var
529529 wrapper: TWrapper;
530530 begin
531531 wrapper := TWrapper.Create;
532532 HandleWrapper(wrapper);
533533 end.
534534```
535535
536536## COMPILER: Implemention Details
537537
538538Default properties are implemented primarily at the parser layer and overloads (proc and operator) are handled through tcallcandiates (must be explictly requested in create params). They could be handled in the node layer during typechecking instead if that was deemed more appropriate. I used the parser layer because regular properties are handled this way and I wasn't sure about the ramifications of making nodes convert themselves to properties in all instances across the entire platform. Furthermore I personally see default properties as a sort of "meta-layer" which doesn't really exist but is more of a re-routing scheme which is applied in advance of normal syntax.
539539
540540For reference here are some of the important changes made to support default properties.
541541
542542- symtable.pas
543543
544544searchsym_xxx() functions now contain parameters which explicitly request defaults to be included in the result (see ssf_search_defaults) and return a tpropertysym. searchsym_with_defaults() is added for unit level searching.
545545
546546- pexpr.pas
547547
548-` do_member_read_internal() - handles the default property sym returned by searchsym_xxx using handle_read_property().`\
549-` do_proc_call() - after proc params are parsed default read property is handled via handle_default_proc_call().`\
550-` `
548+```text
549+ do_member_read_internal() - handles the default property sym returned by searchsym_xxx using handle_read_property().
550+ do_proc_call() - after proc params are parsed default read property is handled via handle_default_proc_call().
551+```
551552
552553Other relevant functions:
553554
554-` handle_default_assignment()`\
555-` handle_default_proc_call()`\
556-` handle_default_unary_operator()`\
557-` handle_default_binary_operator()`
555+```text
556+ handle_default_assignment()
557+ handle_default_proc_call()
558+ handle_default_unary_operator()
559+ handle_default_binary_operator()
560+```
558561
559562- htypcheck.pas
560563
561564Procdefs returned from tcallcandidates (if requested in constructor) can return procdefs from default properties. To help facilitate this tprocoverloadentry class was added (see collect_overloads_in_struct).
562565
563566The new overload for choose_best will return a tpropertysym (if the the best procdef belongs to a default property) so it can be handled properly.
564567
565568- pstatmnt.pas
566569
567570Expressions of type torddef are converted to properties using handle_expr_default_property_access().
568571
569572- nutils.pas
570573
571574Various helper functions for default properties:
572575
573-` handle_read_property()`\
574-` handle_write_property()`\
575-` try_default_read_property()`
576+```text
577+ handle_read_property()
578+ handle_write_property()
579+ try_default_read_property()
580+```
576581
577582- nflw.pas
578583
579584Uses try_default_read_property() to resolve default read properties for for..in enumerators. This is only possible for default properties that support the for..in loop, such as arrays, sets etc...
580585
581586---
582587*From the Free Pascal wiki, page "default properties", as it stood on 2019-12-16 after a change by Trev, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
583588Category: FPC

Back to history