"Flatten" linked-lists or trees in Watches of debugger lazarus / 12-Debugger ---------------------------------------------------------------------- Martin_fr · 2024-07-22 14:10 Lazarus 3.99 (FpDebug only) For review: https://wiki.freepascal.org/FpDebug-Watches-Intrinsic- Functions#flatten See description in link. Displays linked lists, and trees, etc as array in the Watches Window Comments on naming, options, etc welcome. ---------------------------------------------------------------------- 440bx · 2024-07-22 14:49 call me lazy but, it would be great if you posted the code of the little example too.  That would enable the "reviewer" to initially be on the same page as you are and not run the risk of changing the code due to a typo or mental lapse. question: my version of trunk is two days old, (downloaded on the 20th), is that recent enough to test/review this feature or should I update to today's version ? ---------------------------------------------------------------------- Martin_fr · 2024-07-22 14:57 Attached has some demo data and watches ---------------------------------------------------------------------- Martin_fr · 2024-07-22 14:59 Quote from: 440bx on July 22, 2024, 04:49:55 pmquestion: my version of trunk is two days old, (downloaded on the 20th), is that recent enough to test/review this feature or should I update to today's version ? Todays. ---------------------------------------------------------------------- Martin_fr · 2024-07-22 15:01 And, one possible obvious suggestion. When walking a tree (e.g. with left/right fields), it may be useful to add info to each result item, if it was a left or right key. But that is for another day. ---------------------------------------------------------------------- 440bx · 2024-07-22 20:25 Hi Martin, I've got a comment/request/suggestion about the :flatten feature. I think it is a very nice feature.  It provides an array view of what may be a multilevel tree.  That can sometimes be very useful. For reference, I used the code you first posted (the example's simplicity makes visualizing the operations easier.)Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i {$APPTYPE CONSOLE} program _Flatten; type  TFoo = class    Value : integer;    Next  : TFoo;  end; var  f, f1 : TFoo;  i     : integer; begin  f  := TFoo.Create;  f1 := f;   for i := 1 to 5 do  begin    f1.Value := i;  f1.Next  := TFoo.Create;    f1       := f1.Next  end;   f.Value := -1;end.                                 Running the code to the "end." produces the attached Watches screenshot. While the "array"/"flat" view is very nice to have, the dependency tree is lost and that can sometimes be a problem.  What I'd like to see is, instead of having the node be numbered "1", I'd like to see it labeled as "0.0", node "2" labeled "0.0.0" (instead of "2") and so on, that way the label/(array index) gives an indication of where in the tree the element belongs without having to navigate the actual tree.  That way if a branch has multiple children then they would be labeled "0.0.0", "0.1.0", "0.2.0", etc presuming the branch has 2 children which in turn have one child each.    That would give the best of both worlds, the flat array view and an indicator of where the node belongs in the tree structure. I realize that what I'm suggesting may not be possible if the tree uses numbers instead of text labels but, I thought it wouldn't hurt to mention it. Lastly, I think displaying the _vptr$TOBJECT (or whatever object is inherited from) could be made optional.  The idea is that the flattening operation is done to see the link tree not the objects (but, I could be wrong about that, since as most everyone knows, I don't OOP ) Hopefully some of this is useful to you. ---------------------------------------------------------------------- Martin_fr · 2024-07-22 21:05 Quote from: 440bx on July 22, 2024, 10:25:16 pmWhat I'd like to see is, instead of having the node be numbered "1", I'd like to see it labeled as "0.0", node "2" labeled "0.0.0" (instead of "2") and so on, that way the label/(array index) gives an indication of where in the tree the element belongs without having to navigate the actual tree.  That way if a branch has multiple children then they would be labeled "0.0.0", "0.1.0", "0.2.0", etc presuming the branch has 2 children which in turn have one child each.  It all depends on what is stored and why you traverse it. One use case I would have had in the past, was an AVL tree with words, and I wanted to know if a certain word is in it. It would not have mattered where in the tree it was. Searching a tree with maybe 300 nodes really not a pleasure, browsing an array that size would have been better. [1] As for the numbering. The result is an array. The index is not part of the data. So there can't be custom values in the index. But I have a similar idea, and the solution would be that each array entry is an artificial record, with the fields "k" for the key-info, and "d" for the data (current returned data). My idea was actually to have a "depth"(d) and the "keyname"(k) (or "key index") and the data(o) So it would be like Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i d: 1, k: Left, o: ....d: 2, k: Left, o: ....d: 3, k: Left, o: ....d: 2, k: Right, o: ....d: 3, k: Left, o: ....d: 1, k: Right, o: ....d: 2, k: Left, o: .... If the keys were expressions, then their index might be preferred (chosen via option), to avoid a long expression show in the result But both ways can be offered. Yours has merits too. Alas, as indicated: Work for another day. QuoteLastly, I think displaying the _vptr$TOBJECT (or whatever object is inherited from) could be made optional.  The idea is that the flattening operation is done to see the link tree not the objects (but, I could be wrong about that, since as most everyone knows, I don't OOP ) That is more a general issue of how TObject is displayed. Also, it may not be as much of an issue as though in first. Usually the interest is not on the List/Tree node/container, but the data. If the Data is stored an a field of its own then it can be displayed standalone. In the example the Data is an integer on the field "Value" (But it could be an object on a field "Item"). Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(f, Next)[0..999].Value And what that will do, is just extract the values. Though if you have info like nil, or errors in the list, they will be replaced with a new, meaningless error..... And of course, if the above idea (numeration) is implemented, then you can't use this mapping as you loose the fields with the "location". (Unless there is a way for a custom mapping, which there may one day be) [1] And that brings me to the idea of creating an IndexOf intrinsic ---------------------------------------------------------------------- dbannon · 2024-07-23 00:43 Another excellent addition to the debugger. I am amazed how much it has improved in the last couple of years ! Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i flatten(f, Next) In your example, you offer two parameters to flatten(), is the "Next" there to tell the function to show the value of next or to tell it how to get to the the next item in the list ? In most cases (not all) I'm not really interested in the value of Next, all I want to see is the data in the item. Having the pointer value there is certainly correct but does make it all just a bit harder to read sometimes. Davo ---------------------------------------------------------------------- Martin_fr · 2024-07-23 07:51 Quote from: dbannon on July 23, 2024, 02:43:07 amIn your example, you offer two parameters to flatten(), is the "Next" there to tell the function to show the value of next or to tell it how to get to the the next item in the list ? In most cases (not all) I'm not really interested in the value of Next, all I want to see is the data in the item. Having the pointer value there is certainly correct but does make it all just a bit harder to read sometimes. You can have any amount of parameters (up to 999 different fields); Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten( MyTree, Left, Right ) :flatten retrieves all the objects/records. It needs "Next" as the name of the field that it needs to follow. It is coincidence that in the example the link to the next object is in a field called "Next". To just display the data in each node, it can be combined with [0..9999]. So if you have a tree, with Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i type TTreeNode = class  Left, Right: TTreeNode;  Data: String; // or another class, or...endThen you can do Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(node, Left, Right)[0..9999].Data And you get only the Data of each node. The [..] array slice is also described on the linked page about intrinsics. It adds the magic to allow mapping each entry in the array. In my image, in the first post, I watch the same linked list twice, the 2nd time with mapping I only get the numbers. ---------------------------------------------------------------------- dbannon · 2024-07-23 11:14 yep, that is seriously useful ! Presently, I would use a debug function added to the unit that dumps the data to stdout.  Will be so much easier you way. Thanks ! Davo ---------------------------------------------------------------------- Martin_fr · 2024-07-24 13:05 Arrays and Array-Slices... So I am adding expansion of arrays. Optional. Arrays: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i type  PBar = ^TBar;  TBar = record    Data: array[1..4] of integer;    Others: array[1..4] of PBar;    //MorePtr: PBar; // pointer to list of PBar, so MorePtr[0..9] will work    //More: array[1..2, 1..4] of PBar;    Next: PBar;  end;var  MyBar: PBar; begin  MyBar := new(PBar);           MyBar^             := Default(TBar); with MyBar^             do begin Data[1] := 101; Data[2] := 102; end;  MyBar^.Next := new(PBar);     MyBar^.Next^       := Default(TBar); with MyBar^.Next^       do begin Data[1] := 201; Data[2] := 202; end;  MyBar^.Next^.Next := new(PBar); MyBar^.Next^.Next^ := Default(TBar); with MyBar^.Next^.Next^ do begin Data[1] := 301; Data[2] := 302; end;   MyBar^.Others[1] := new(PBar);  MyBar^.Others[1]^ := Default(TBar);  with MyBar^.Others[1]^ do begin Data[1] := 111; Data[2] := 112; end;  MyBar^.Others[2] := new(PBar);  MyBar^.Others[2]^ := Default(TBar);  with MyBar^.Others[2]^ do begin Data[1] := 121; Data[2] := 122; end;  MyBar^.Others[3] := new(PBar);  MyBar^.Others[3]^ := Default(TBar);  with MyBar^.Others[3]^ do begin Data[1] := 131; Data[2] := 132; end;  MyBar^.Others[4] := new(PBar);  MyBar^.Others[4]^ := Default(TBar);  with MyBar^.Others[4]^ do begin Data[1] := 141; Data[2] := 142; end;   MyBar^.Next^.Others[1] := new(PBar); MyBar^.Next^.Others[1]^ := Default(TBar); with MyBar^.Next^.Others[1]^ do begin Data[1] := 211; Data[2] := 212; end;  MyBar^.Next^.Others[2] := new(PBar); MyBar^.Next^.Others[2]^ := Default(TBar); with MyBar^.Next^.Others[2]^ do begin Data[1] := 221; Data[2] := 222; end;  MyBar^.Next^.Others[3] := new(PBar); MyBar^.Next^.Others[3]^ := Default(TBar); with MyBar^.Next^.Others[3]^ do begin Data[1] := 231; Data[2] := 232; end;  MyBar^.Next^.Others[4] := new(PBar); MyBar^.Next^.Others[4]^ := Default(TBar); with MyBar^.Next^.Others[4]^ do begin Data[1] := 241; Data[2] := 242; end;   //MyBar^.Others[2]^.Others[1] := new(PBar); MyBar^.Others[2]^.Others[1]^ := Default(TBar); with MyBar^.Others[2]^.Others[1]^ do begin Data[1] := 211; Data[2] := 212; end;  //MyBar^.Others[2]^.Others[2] := new(PBar); MyBar^.Others[2]^.Others[2]^ := Default(TBar); with MyBar^.Others[2]^.Others[2]^ do begin Data[1] := 221; Data[2] := 222; end;  Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(MyBar^, Next, Others)Would normally return Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i Len=7: ((Data: (101, 102, 0, 0);Others: ($0000000000117570^: (), $00000000001175B0^: (), $00000000001175F0^: (), $0000000000117630^: ());Next: $00000000001174F0^: ();),(Data: (201, 202, 0, 0);Others: ($0000000000117670^: (), $00000000001176B0^: (), $00000000001176F0^: (), $0000000000117730^: ());Next: $0000000000117530^: ();),(Data: (301, 302, 0, 0);Others: (nil, nil, nil, nil);Next: nil;),nil,(nil, nil, nil, nil),($0000000000117670^: (), $00000000001176B0^: (), $00000000001176F0^: (), $0000000000117730^: ()),($0000000000117570^: (), $00000000001175B0^: (), $00000000001175F0^: (), $0000000000117630^: ())) Or for "Data" Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(MyBar^, Next, Data)Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i Len=7: ((Data: (101, 102, 0, 0);Others: ($0000000000117570^: (), $00000000001175B0^: (), $00000000001175F0^: (), $0000000000117630^: ());Next: $00000000001174F0^: ();),(Data: (201, 202, 0, 0);Others: ($0000000000117670^: (), $00000000001176B0^: (), $00000000001176F0^: (), $0000000000117730^: ());Next: $0000000000117530^: ();),(Data: (301, 302, 0, 0);Others: (nil, nil, nil, nil);Next: nil;),nil,(301, 302, 0, 0),(201, 202, 0, 0),(101, 102, 0, 0)) That is, in each case, when the key returned an array, that array is include "as is". That is certainly good for "Data" (where you likely don't want each of the numbers to become a row of its own in the flatten-result). For "Others" it depends on the use case. Like, if you had the keys "Next" and "Previous", then you would only list "Next", because "Previous" should already be in the list. So you may or may not want to specify the array as . You can select individual elements of an array (no ranges/slices => see below) Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(MyBar^, Next, Others[2], Others[4]) But if you want all keys, that can be a lot of work, and if the length is dynamic then it wont work that way at all. So you will be able to do Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(MyBar^, Next, Others, [array]):f_(MyBar^, Next, Others, [array=1]):f_(MyBar^, Next, Others, [array=2]) The first do are the same. (default is "1"). Expand arrays down to the given nesting depth.  - Depth applies to directly nested arrays: MyBar^.More[1][2] (requires array=2 or higher)  - Depth starts over, if there are objects in between MyBar^.Others[1]^.Others[2] (fine with array=1 because Others[1] did return an object, not a nested array) Array-slices: Array slices may be handled different... Internally Array slices need different handling anyway. So it comes at no cost to give separate control to the user. That way you could allow to expand slices, but not arrays, and then you could do (if implemented) Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(MyBar^, Next, Others[1..4], Data, [slice])Which would expand "Others", but not "Data". But then again, there is the Question: Maybe slices should always be expanded? For that lets have a look how they work, if not expanded. (i.e. currently): A slice produces an array result, which can be mapped. That is any operation on the array-result is performed on each entry in it. - foo[1..4].bar => return an array containing only the field bar of each of the 4 (object/record) value from the array foo. - :lenght(foo[1..4]) => returns an array containing the :length of each of the 4 (hopfully string/array) values from the array foo. And then: :_flatten(foo[1..4], Next) returns an array with 4 entries, each entry being one flattened array for each of the 4 value in foo. But also: :_flatten(MyBar, Others[1..4]) returns an array with 4 entries, each entry being one flattened array. The first flattened array is flattened using "Others[1]". The 2nd flattened array is flattened using "Others[2]". The 3rd ... Question: Does that last example ever really make sense? (And if it does, does it so much that it should be default?) IHMO, the first example (slice in the first param) could be sometimes useful, but probably in most cases wants to be flattened into ONE flattened result? But the 2nd example (slice in the key) almost never (if ever) should return 4 results. It should flatten into one list. Does that make sense? Or am I overlooking use cases? Based on that, my current idea would be to have all slices inside a ":flatten" being flattened (rather than resulting in multiple separate flattened results). If such behaviour should be override-able in future (not sure if it will), then there would be 2 ways. - :flaten(a, b[1..2], c, [noslice]) - :flaten(a, :slice(b[1..2]), c)  // wrap the key into an intrinsic that will make the slice not being flattened Of course, if slices by default will not be flattened, then ":slice()" could mean, for this param include slices into ONE flatten result. => In any case, slices will not have a "depth", but be either all one way , or all the other way. => They will likely use something like ":slice()" and not "[noslice]". Because the latter has some complications for the implementation. So some thought, and some fixes later... It will be a syntax, not an option. It will come down to 1) The current Syntax defaults to multiple flatten, or integrated into one flatten. 2) What syntax?  wrapper?  :f_( a, :x(next[1..3]) ) The difference in result is, that ** multiple flatten for :f_( a, next[1..3]) returns  a NESTED array with the 3 outer entries from the slice, each holding ONE of the flattens    :f_( a, next[1]),    :f_( a, next[2]),    :f_( a, next[3]) ** integrated one flatten for :f_( a, :x(next[1..3]) return on non-nested array    a.next[1],    a.next[1].next[1],    a.next[1].next[1].next[1],   ...    a.next[1].next[2],    ...    a.next[1].next[3],    ...    a.next[2],    a.next[2].next[1],    ...    a.next[3],   ... As for amending the syntax of the slice, I don't want to touch the  [n..m] part itself. That part is know by the FpDebug, but also by the IDE. If you have a watch "a[1..3]" in the watches window, and "unfold" it, then the IDE must be able to create the sub-watches; "a[1]", "a[2]", "a[3]" ---------------------------------------------------------------------- Martin_fr · 2024-07-24 21:02 A similar issue may arise with other future intrinsics. E.g. there may be a   :IndexOf(array,  match)  that will return if (and where) a matching element was first found. Using   :IndexOf(data[0..99],  match)    can do either - search entries 0 to 99 - if data is "array of array" for each of the first 100 entries perform a separate IndexOf, and return 99 indexes. It may even be that outside such functions, one might want to stop the "mapping" of a slice.    data[0..99].foo[3] - retrieves an array in the field foo for each of the entries, and get the 3rd entry from that foo array. - to only get the 3rd entry of the map, one has to change the [0..99] to [3]. But :stop(data[0..99].foo)[3]  could mean the entry of the slice. Such a "slice map stopper" would also work inside :flatten. Though maybe :flatten itself should be a stopper, and there could be a marker(wrapper) for slices that should not be stopped?  :flatten(a[5..9], :extslice(next[1..3]))  => would create 3 flattens, for each "next[1]", "next[2]" , "next[3]", but the slice an "a" would stay inside the flatten. ---------------------------------------------------------------------- Martin_fr · 2024-07-26 19:14 Quote from: 440bx on July 22, 2024, 10:25:16 pmWhile the "array"/"flat" view is very nice to have, the dependency tree is lost and that can sometimes be a problem.  What I'd like to see is, instead of having the node be numbered "1", I'd like to see it labeled as "0.0", node "2" labeled "0.0.0" (instead of "2") and so on, that way the label/(array index) gives an indication of where in the tree the element belongs without having to navigate the actual tree.  That way if a branch has multiple children then they would be labeled "0.0.0", "0.1.0", "0.2.0", etc presuming the branch has 2 children which in turn have one child each.  Added something... Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(a,b, [obj1])There are obj1, obj2, obj3 and obj4. The default is - if only one key is followed, then: OFF - if more than one key is given to follow, then: obj1 Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i program test_flatten;type  TFoo = class    Value: integer;    Next: TFoo;    More: array[5..7] of TFoo;  end;var  foo: TFoo;  m, n: Integer;begin  foo                := TFoo.Create; foo.Value := 1;  foo.Next         := TFoo.Create; foo.Next.Value := 2;  foo.Next.Next      := TFoo.Create; foo.Next.Next.Value := 3;  foo.Next.Next.Next := TFoo.Create; foo.Next.Next.Next.Value := 4;   for m := 5 to 7 do for n := 6 to 7 do begin    foo.More[m] := TFoo.Create;              foo.More[m].Value              := m*11000000 + 1;    foo.More[m].More[n] := TFoo.Create;      foo.More[m].More[n].Value    := m*11000000 + 11;    foo.More[m].Next := TFoo.Create;         foo.More[m].Next.Value         := m*11000000 + 9901;    foo.More[m].Next.More[n] := TFoo.Create; foo.More[m].Next.More[n].Value := m*11000000 +9911;    foo.Next.More[m] := TFoo.Create; foo.Next.More[m].Value := m*10100000 + 2;  end;  m := 1; // breakpointend. OBJ1 will print the DEPTH and the KEY (or expression) Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(foo, next, more, [array, obj1])Len=19: ((d: 0;k: '';v: TFoo(Value: 1;Next: $00000000015774F0;More: ($00000000015776F0, $0000000001577970, $0000000001577BF0);_vptr$TOBJECT: $000000010000F000;);),(d: 0;k: 'next';v: TFoo(Value: 2;Next: $0000000001577530;More: ($00000000015777F0, $0000000001577A70, $0000000001577CF0);_vptr$TOBJECT: $000000010000F000;);),(d: 1;k: 'next';v: TFoo(Value: 3;Next: $0000000001577570;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 'next';v: TFoo(Value: 4;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 'more[0]';v: TFoo(Value: 50500002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 'more[1]';v: TFoo(Value: 60600002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 'more[2]';v: TFoo(Value: 70700002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 1;k: 'more[0]';v: TFoo(Value: 55000001;Next: $0000000001577770;More: (nil, nil, $0000000001577730);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 'next';v: TFoo(Value: 55009901;Next: nil;More: (nil, nil, $00000000015777B0);_vptr$TOBJECT: $000000010000F000;);), OBJ2 will print the DEPTH and the index of the KEY (or expression) Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(foo, next, more, [array, obj2])Len=19: ((d: 0;k: -1;v: TFoo(Value: 1;Next: $00000000015774F0;More: ($00000000015776F0, $0000000001577970, $0000000001577BF0);_vptr$TOBJECT: $000000010000F000;);),(d: 0;k: 0;v: TFoo(Value: 2;Next: $0000000001577530;More: ($00000000015777F0, $0000000001577A70, $0000000001577CF0);_vptr$TOBJECT: $000000010000F000;);),(d: 1;k: 0;v: TFoo(Value: 3;Next: $0000000001577570;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 0;v: TFoo(Value: 4;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 1;v: TFoo(Value: 50500002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 1;v: TFoo(Value: 60600002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 2;k: 1;v: TFoo(Value: 70700002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(d: 1;k: 1;v: TFoo(Value: 55000001;Next: $0000000001577770;More: (nil, nil, $0000000001577730);_vptr$TOBJECT: $00000 OBJ3/4 follow your idea. That works for tree structures, but has the disadvantage that the 0.0.0.0.0.0.0.0. will become very long an a pure linked list. Such a linked list with 1000 entries gets 1000 * "0.". It will be cut if at a string-length of 1000. OBJ3 uses names Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(foo, next, more, [array, obj3])Len=19: ((k: '';v: TFoo(Value: 1;Next: $00000000015774F0;More: ($00000000015776F0, $0000000001577970, $0000000001577BF0);_vptr$TOBJECT: $000000010000F000;);),(k: 'next';v: TFoo(Value: 2;Next: $0000000001577530;More: ($00000000015777F0, $0000000001577A70, $0000000001577CF0);_vptr$TOBJECT: $000000010000F000;);),(k: 'next.next';v: TFoo(Value: 3;Next: $0000000001577570;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: 'next.next.next';v: TFoo(Value: 4;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: 'next.more[0]';v: TFoo(Value: 50500002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: 'next.more[1]';v: TFoo(Value: 60600002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: 'next.more[2]';v: TFoo(Value: 70700002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: 'more[0]';v: TFoo(Value: 55000001;Next: $0000000001577770;More: (nil, nil, $0000000001577730);_vptr$TOBJECT: $000000010000F000;);),(k: 'more[0].next';v: TFoo(Value: 55009901;Next: nil;More: (nil, nil, $00000000015777B0);_vptr$TOBJECT: $000000010000F000;);),(k: 'more[0].next.more[2]';v: TFoo(Value: 55009911;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),  OBJ4 uses indexes (your idea) / in this example mixed with array expansion, which adds the indexes in "[]" Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(foo, next, more, [array, obj4])Len=19: ((k: '';v: TFoo(Value: 1;Next: $00000000015774F0;More: ($00000000015776F0, $0000000001577970, $0000000001577BF0);_vptr$TOBJECT: $000000010000F000;);),(k: '0';v: TFoo(Value: 2;Next: $0000000001577530;More: ($00000000015777F0, $0000000001577A70, $0000000001577CF0);_vptr$TOBJECT: $000000010000F000;);),(k: '0.0';v: TFoo(Value: 3;Next: $0000000001577570;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: '0.0.0';v: TFoo(Value: 4;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: '0.1[0]';v: TFoo(Value: 50500002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: '0.1[1]';v: TFoo(Value: 60600002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: '0.1[2]';v: TFoo(Value: 70700002;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);),(k: '1[0]';v: TFoo(Value: 55000001;Next: $0000000001577770;More: (nil, nil, $0000000001577730);_vptr$TOBJECT: $000000010000F000;);),(k: '1[0].0';v: TFoo(Value: 55009901;Next: nil;More: (nil, nil, $00000000015777B0);_vptr$TOBJECT: $000000010000F000;);),(k: '1[0].0.1[2]';v: TFoo(Value: 55009911;Next: nil;More: (nil, nil, nil);_vptr$TOBJECT: $000000010000F000;);), ---------------------------------------------------------------------- Martin_fr · 2024-07-26 19:27 About specifying the options array,err,obj1... If non are specified, then default is used (all, expect obj or including obj if multi key). But the "default set" could be assumed anything. Options that take a number (array=1, max = 1000) are exempt from below. Then options can be specified as Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i flatten(a,b, [nil, deref=true, seen=false])This starts with an empty set of options. - an option without modifier is included (true) - an option with =true is included - an option with =false  is NOT included   (could be used, if true/false is computed from a variable in the debugged app) Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i flatten(a,b, +[nil, deref=true, seen=false])Note the "+" This starts with the default set of options and adds options. The same true/false rules apply. (So using =false this can also remove options) Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i flatten(a,b, -[nil, deref=true, seen=false])Note the "-" This starts with the default set of options and removes options. - an option without modifier is removed - an option with =true is removed - an option with =false  is ADDED The last one is because the "-" reverses all operations. Comments? There would be an alternative to interpret the "-": Only modify the meaning of an identifier without =true/false. I.e., normally just "nil" translates to "nil=true", but with a "-" it translates to "nil=false". And then explicit true/false would not be inverted by the "-". Thoughts? ---------------------------------------------------------------------- 440bx · 2024-07-26 21:46 I presume these new capabilities require today's trunk version. You're right that the way I suggested works best when the number of branches may be numerous but the tree is shallow.  If the tree isn't shallow (reasonably, about a practical max depth of 16) then the output is somewhat unwieldy.  It's nice for data indexed using B-Trees (depth kept low by using a larger - greater than 2 - number of branches.) Going to download it and give it a try. Thank you for the work you put into it. ---------------------------------------------------------------------- 440bx · 2024-07-26 23:36 Just played with it after updating to the latest trunk. I really like the Obj3 view.  It's a little longer than Obj4 but, it makes it really easy to visualize how the tree is organized as well as why (by identifying the field(s) it followed.) This is really good stuff that can be extremely helpful in debugging complex tree indexes. again, thank you for all that work, much appreciated. ---------------------------------------------------------------------- Martin_fr · 2024-07-29 21:23 Brainstorming.... So, if you use Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(a, Left, Right, [obj]) Then you can't do Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(a, Left, Right, [obj])[0..999].SomeValue Because the result of flatten is a structure with the keys d,k,v. And if you append "[0..999].V.SomeValue" to access SomeValue in V, then you loose the structure keys... Solution is simple Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(a, Left"add .SomeValue here", Right, [obj])Of course you can't, because you need the object in the key "Left", so that the object can be used in the next iteration of flatten. (SomeValue may be an int or string on that object) What is needed is, that the key has 2 parts 1) get the object (which will be used for further iteration) 2) get the display part from that object (that will be in "v" of the [obj]) So a separator is needed between the 2 parts of the key. I personally favour the ":" (as it is in var a:int;) Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(a, Left:SomeValue, Right:SomeValue, [obj])Which means, get the next object for the iteration from Left and Right. But in the output show only Left.SomeValue. So far so good. Only downside: the ":" is also used in Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i a ? b : cAnd the colon can start an intrinsic. Yet, there is no case where that would make it ambiguous. It can always be clearly detected as one and only one of those. But it is like the "case else" if you have an large nested "if then else" in the case section before. E.g. you can have Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :f_(a,    :_.x ? :_.Left1 : :_.Left2    :    :_.y ? :_.SomeValue1 : :_.SomeValue2,    Right:SomeValue,    [obj]) On the other hand I really think the colon is the most natural choice... ---------------------------------------------------------------------- Martin_fr · 2024-07-31 21:16 I added :obj And mapping for each key. See the wiki. ---------------------------------------------------------------------- Martin_fr · 2024-08-02 20:25 And I got the "!" working. To end the magic-mapping of a slice, and allow it to be used inside :flatten. Now if you have a structure as below, you can do: Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i :flatten(MyData, :cc(TObject(:_.Children.FList.FList^[0..:_.Children.F List.FCount-1]))!: :obj(val: :_.Value, dat: :_.Data), [array,obj4]) Mind, if you got more than one key to follow (e.g. if there was a BackupChildren that you also wanted to include), then you do not need to repeat the ": :obj(....)" part. Instead you can do "BackupChildren:1" The "1" meaning to use the same expression for display as the argument with index=1) I am aware that the initial object does not get mapped.... yet... But, well, later. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i program test;uses Classes;type  TFoo = class    Value: integer;    Data: boolean;    constructor Create(V: integer; D: Boolean); overload;  end;   TFooNode = class(TFoo)    Children: TList;    constructor Create(V: integer; D: Boolean); overload;  end; constructor TFoo.Create(V: integer; D: Boolean);begin  Value := v;  Data := d;end; constructor TFooNode.Create(V: integer; D: Boolean);begin  inherited Create(V, D);  Children := TList.Create;end; var  MyData: TFooNode;begin  MyData := TFooNode.Create(1,True);  TFooNode(MyData).Children.Add(TFooNode.Create(11, False));  TFooNode(MyData).Children.Add(TFooNode.Create(12, False));  TFooNode(MyData).Children.Add(TFooNode.Create(13, True));  TFooNode(MyData).Children.Add(TFooNode.Create(14, False));   TFooNode(MyData.Children[0]).Children.Add(TFooNode.Create(111, False));  TFooNode(MyData.Children[0]).Children.Add(TFooNode.Create(112, True));  TFooNode(MyData.Children[0]).Children.Add(TFooNode.Create(113, False));   TFooNode(MyData.Children[1]).Children.Add(TFooNode.Create(121, False));  TFooNode(MyData.Children[1]).Children.Add(TFooNode.Create(122, False));  TFooNode(MyData.Children[1]).Children.Add(TFoo.Create(123, False));  TFooNode(MyData.Children[1]).Children.Add(TFoo.Create(124, False));   TFooNode(TFooNode(MyData.Children[1]).Children[1]).Children .Add(TFooNode.Create(1221, False));  TFooNode(TFooNode(MyData.Children [1]).Children[1]).Children.Add(TFooNode.Create(1221, True));   TFooNode(MyData.Children[3]).Children.Add(TFooNode.Create(141, True));  MyData:=MyData; // break here end.