I6 Template Layer

Inform 7 6M62ContentsIntroductionFunction IndexRules Index

ListWriter.i6t

ListWriter contents

Specification.

The list-writer is called by one of the following function calls:

(1) WriteListOfMarkedObjects(style), where the set of objects listed is understood to be exactly those with the workflag2 attribute set, and (-a) the style is a sum of *_BIT constants as defined in Definitions.i6t.

(2) WriteListFrom(obj, style, depth, noactivity, iterator), where only the first two parameters are compulsory: (-a) the set of objects listed begins with obj; (-b) the style is a sum of *_BIT constants as defined in Definitions.i6t; (-c) the depth is the recursion depth within the list – ordinarily 0, but by supplying a higher value, we can simulate a sublist of another list; (-d) noactivity is a flag which forces the list-writer to ignore the "listing the contents of" activity (in cases where it would otherwise consult this): by default this is false; (-e) iterator is an iterator function which provides the objects in sequence.

WriteListOfMarkedObjects is simply a front-end which supplies suitable parameters for WriteListFrom.

The iterator function is by default ObjectTreeIterator. This defines the sequence of objects as being the children of the parent of obj, in object tree sequence (that is: child(parent(obj)) is first). Moreover, when using ObjectTreeIterator, the "listing the contents of" activity is carried out, unless noactivity is set.

We also provide the iterator function MarkedListIterator, which defines the sequence of objects as being the list in the word array MarkedObjectArray with length MarkedObjectLength. Here the "listing the contents of" activity is never used, since the objects are not necessarily the contents of any single thing. This of course is the iterator used by WriteListOfMarkedObjects(style): it works by filling this array with all the objects having workflag2 set.

The full specification for iterator functions is given below.

The list-writer automatically groups adjacent and indistinguishable terms in the sequence into plurals, and carries out the "printing the plural name" activity to handle these. Doing this alone would result in text such as "You can see a cake, three coins, an onion, and two coins here", where the five coins are mentioned in two clauses because they happen not to be adjacent in the list. The list-writer therefore carries out the activity "grouping together" to see if the user would like to tie certain objects together into a single entry: what this does is to set suitable list_together properties for the objects. NI has already given plural objects a similar list_together property. The net effect is that any entries in the list with a non-blank value of list_together must be adjacent to each other.

We could achieve that by sorting the list in order of list_together value, but that would result in drastic movements, whereas we want to upset the original ordering as little as possible. So instead we use a process called coalescing the list. This is such that for every value L ≠ 0 of list_together, every entry with that value is moved back in the list to follow the first entry with that value. Thus if the original order is x1, x2, ..., xN then xj still precedes xk in coalesced order unless there exists i<j<k such that L(i) = L(k) ≠ 0 and L(j) ≠ L(i). This is as stable as it can be while achieving the "interval" property that non-zero L values occur in contiguous runs.

We therefore obtain text such as "You can see a cake, five coins, the tiles W, X, Y and Z from a Scrabble set, and an onion here." where the coins and the Scrabble tiles have been coalesced together in the list.

It's important to note that the default ObjectTreeIterator has the side-effect of actually reordering the object tree: it rearranges the children being listed so that they appear in the tree in coalesced order. The MarkedListIterator used by WriteListOfMarkedObjects has the same side-effect if the marked objects all happen to share a common parent. It might seem odd for a list-writer to have side effects at all, but the idea is that occasional coalescing improves the quality of play in many small ways – for instance, the sequence of matches to TAKE ALL is tidier – and that coalescing has a small speed cost, so we want to do it as little as possible. (The latter was more of a consideration for I6: interpreters are faster nowadays.)

This specification is somewhat stronger than that of the I6 library's traditional list-writer, because (i) it supports arbitrary lists of objects, not just children of specific parents, while still allowing coalesced and grouped lists, (ii) it can be used recursively in all cases, (iii) it uses its own memory, rather than borrowing memory from the parser, so that it can safely be used while the parser is working, and (iv) it manages this memory more flexibly and without silently failing by buffer overruns on unexpectedly large lists.

The I7 version of WriteListFrom, when using ObjectTreeIterator, differs from the I6 version in that the object o is required to be the child of its parent, that is, to be the eldest child. (So in effect it's a function of parents, not children, but we retain the form for familiarity's sake.) In practice this was invariably the way WriteListFrom was used even in I6 days.

Finally, the ISARE_BIT is differently interpreted in I7: instead of printing something like " are ducks and drakes", as it would have done in I6, the initial space is now suppressed and we instead print "are ducks and drakes".

Memory.

The list-writer needs to dynamically allocate temporary array space of a known size, in such a way that the array is effectively on the local stack frame: if only either the Z-machine or Glulx supported a stack in memory, this would be no problem, but they do not and we must therefore use the following.

The size of the stack is such that it can support a list which includes every object and recurses in turn to most other objects: in practice, this never happens. It would be nice to allocate more just in case, but the Z-machine is desperately short of array memory.

117Constant REQUISITION_STACK_SIZE = 3*{-value:Instances::count(K_object)}; 118Array requisition_stack --> REQUISITION_STACK_SIZE; 119Global requisition_stack_pointer = 0; 120 121[ RequisitionStack len top addr; 122    top = requisition_stack_pointer + len; 123    if (top > REQUISITION_STACK_SIZE) return false; 124    addr = requisition_stack + requisition_stack_pointer*WORDSIZE; 125    ! print "Allocating ", addr, " at pointer ", requisition_stack_pointer, "^"; 126    requisition_stack_pointer = top; 127    return addr; 128]; 129 130[ FreeStack addr; 131    if (addr == 0) return; 132    requisition_stack_pointer = (addr - requisition_stack)/WORDSIZE; 133];

WriteListOfMarkedObjects.

This routine will use the MarkedListIterator. That means it has to create an array containing the object numbers of every object with the workflag2 attribute set, placing the address of this array in MarkedObjectArray and the length in MarkedObjectLength. Note that we preserve any existing marked list on the stack (using the assembly-language instructions @push and @pull) for the duration of our use.

While the final order of this list will depend on what it looks like after coalescing, the initial order is also important. If all of the objects have a common parent in the object tree, then we coalesce those objects and place the list in object tree order. But if not, we place the list in object number order (which is essentially the order of traversal of the initial state of the object tree: thus objects in Room A will all appear before objects in Room B if A was created before B in the source text).

152Global MarkedObjectArray = 0; 153Global MarkedObjectLength = 0; 154 155[ WriteListOfMarkedObjects style 156    obj common_parent first mixed_parentage length g gc; 157 158    gc = -2; 159    objectloop (obj ofclass Object && obj has workflag2) { 160        length++; 161        if (first == nothing) { first = obj; common_parent = parent(obj); } 162        else { if (parent(obj) ~= common_parent) mixed_parentage = true; } 163        g = GetGNAOfObject(obj); g = g%3; 164        if (gc == -2) gc = g; 165        else if (gc ~= g) gc = -1; 166    } 167    if (mixed_parentage) common_parent = nothing; 168 169    if (length == 0) { 170        if (style & ISARE_BIT ~= 0) LIST_WRITER_INTERNAL_RM('W'); 171        else if (style & CFIRSTART_BIT ~= 0) LIST_WRITER_INTERNAL_RM('X'); 172        else LIST_WRITER_INTERNAL_RM('Y'); 173    } else { 174        @push MarkedObjectArray; @push MarkedObjectLength; 175        MarkedObjectArray = RequisitionStack(length); 176        MarkedObjectLength = length; 177        if (MarkedObjectArray == 0) return RunTimeProblem(RTP_LISTWRITERMEMORY); 178 179        if (common_parent) { 180            ObjectTreeCoalesce(child(common_parent)); 181            length = 0; 182            objectloop (obj in common_parent) ! object tree order 183                if (obj has workflag2) MarkedObjectArray-->length++ = obj; 184        } else { 185            length = 0; 186            objectloop (obj ofclass Object) ! object number order 187                if (obj has workflag2) MarkedObjectArray-->length++ = obj; 188        } 189 190        WriteListFrom(first, style, 0, false, MarkedListIterator); 191 192        FreeStack(MarkedObjectArray); 193        @pull MarkedObjectLength; @pull MarkedObjectArray; 194    } 195    prior_named_list = length; 196    prior_named_list_gender = gc; 197    return; 198];

List Number and Gender.

As a brief parenthesis, the same algorithm can be used to work out the prior named list number and gender for everything matching a description.

205[ RegardingMarkedObjects 206    obj length g gc; 207    gc = -2; 208    objectloop (obj ofclass Object && obj has workflag2) { 209        length++; 210        g = GetGNAOfObject(obj); g = g%3; 211        if (gc == -2) { 212            gc = g; 213            prior_named_noun = obj; 214        } else if (gc ~= g) gc = -1; 215    } 216    prior_named_list = length; 217    prior_named_list_gender = gc; 218    if (length == 0) { prior_named_noun = nothing; prior_named_list_gender = -1; } 219    return; 220]; 221 222[ RegardingSingleObject obj; 223    prior_named_list = 1; 224    prior_named_list_gender = -1; 225    prior_named_noun = obj; 226]; 227 228[ RegardingNumber n; 229    prior_named_list = n; 230    prior_named_list_gender = -1; 231    prior_named_noun = nothing; 232]; 233 234[ PNToVP gna; 235    if (prior_named_noun == player) return story_viewpoint; 236    if (prior_named_noun) gna = GetGNAOfObject(prior_named_noun); 237    if (((gna%6)/3 == 1) || (prior_named_list >= 2)) return 6; 238    return 3; 239]; 240 241[ PrintVerbAsValue vb; 242    if (vb == 0) print "(no verb)"; 243    else { print "verb "; vb(1); } 244]; 245 246[ VerbIsMeaningful vb; 247    if ((vb) && (BlkValueCompare(vb(CV_MEANING), Rel_Record_0) ~= 0)) rtrue; 248    rfalse; 249]; 250 251[ VerbIsModal vb; 252    if ((vb) && (vb(CV_MODAL))) rtrue; 253    rfalse; 254];

List Writer Regard Storage.

This is somewhat hacky, but enables "[regarding list writer internals]".

260Array LWI_Storage --> 1 (-1) nothing; 261[ SetLWI a b c; 262    LWI_Storage-->0 = a; 263    LWI_Storage-->1 = b; 264    LWI_Storage-->2 = c; 265]; 266[ RegardingLWI; 267    prior_named_list = LWI_Storage-->0; 268    prior_named_list_gender = LWI_Storage-->1; 269    prior_named_noun = LWI_Storage-->2; 270];

Response Printing.

From which:

276[ ResponseViaActivity R; 277    @push prior_named_noun; @push prior_named_list; @push prior_named_list_gender; 278    RegardingSingleObject(nothing); 279    CarryOutActivity(PRINTING_RESPONSE_ACT, R); 280    @pull prior_named_list_gender; @pull prior_named_list; @pull prior_named_noun; 281];

About Iterator Functions.

Suppose Iter is an iterator function and that we have a "raw list" x1, x2, ..., xn of objects. Of these, the iterator function will choose a sublist of "qualifying" objects. It is called with arguments

Iter(obj, depth, L, function)

where the obj is required to be xj for some j and the function is one of the *_ITF constants defined below:

(a) On START_ITF, we return x1, or nothing if the list is empty.

(b) On SEEK_ITF, we return the smallest k ≥ j such that xk qualifies, or nothing if none of xj, xj+1, ..., xn qualify.

(c) On ADVANCE_ITF, we return the smallest k > j such that xk qualifies, or nothing if none of xj+1, xj+2 ..., xn qualify.

(d) On COALESCE_ITF, we coalesce the entire list (not merely the qualifying entries) and return the new x1, or nothing if the list is empty.

Thus, given any xi, we can produce the sublist of qualifying entries by performing START_ITF on xi, then SEEK_ITF, to produce q1; then ADVANCE_ITF to produce q2, and so on until nothing is returned, when there are no more qualifying entries. SEEK_ITF and ADVANCE_ITF always return qualifying objects, or nothing; but START_ITF and COALESCE_ITF may return a non-qualifying object, since there's no reason why x1 should qualify in any ordering.

The iterator can make its own choice of which entries qualify, and of what the raw list is; depth is supplied to help in that decision. But if L is non-zero then the iterator is required to disqualify any entry whose list_together value is not L.

318Constant SEEK_ITF = 0; 319Constant ADVANCE_ITF = 1; 320Constant COALESCE_ITF = 2; 321Constant START_ITF = 3; 322 323! Constant DBLW; ! Uncomment this to provide debugging information at run-time

Marked List Iterator.

Here the raw list is provided by the MarkedObjectArray, which is convenient for coalescing, but not so helpful for translating the obj parameter into the i such that it is xi. We simply search from the beginning to do this, which combined with other code using the iterator makes for some unnecessary O(n2) calculations. But it saves memory and nuisance, and the iterator is used only with printing to the screen, which never needs to be very rapid anyway (because the player can only read very slowly).

335[ MarkedListIterator obj depth required_lt function i; 336    if (obj == nothing) return nothing; 337    if (required_lt == 0) required_lt = EMPTY_TEXT_VALUE; 338    switch(function) { 339        START_ITF: return MarkedObjectArray-->0; 340        COALESCE_ITF: return MarkedListCoalesce(); 341        SEEK_ITF, ADVANCE_ITF: 342            for (i=0: i<MarkedObjectLength: i++) 343                if (MarkedObjectArray-->i == obj) { 344                    if (function == ADVANCE_ITF) i++; 345                    for (:i<MarkedObjectLength: i++) { 346                        obj = MarkedObjectArray-->i; 347                        if ((LT_Compare(required_lt, EMPTY_TEXT_VALUE) ~= 0) && 348                            (LT_Compare(obj.list_together, required_lt) ~= 0)) continue; 349                        if ((c_style & WORKFLAG_BIT) && (depth==0) && (obj hasnt workflag)) 350                            continue; 351                        if ((c_style & CONCEAL_BIT) && (ConcealedFromLists(obj))) continue; 352                        return obj; 353                    } 354                    return nothing; 355                } 356    } 357    return nothing; 358];

Concealment.

This is the definition of concealment used by the list-writer, and means that, for example, the "deciding the concealed possessions" activity is taken into account.

366[ ConcealedFromLists obj c; 367    if ((obj has concealed) || (obj has scenery)) rtrue; 368    c = parent(obj); 369    if ((c) && (c ofclass K5_container or K6_supporter) && (TestConcealment(c, obj))) rtrue; 370    rfalse; 371];

Coalesce Marked List.

The return value is the new first entry in the raw list.

377[ MarkedListCoalesce o i lt l swap m; 378    for (i=0: i<MarkedObjectLength: i++) { 379        lt = (MarkedObjectArray-->i).list_together; 380        if (LT_Compare(lt, EMPTY_TEXT_VALUE) ~= 0) { 381            ! Find first object in list after contiguous run with this list_together value: 382            for (i++: (i<MarkedObjectLength) && 383                (LT_Compare((MarkedObjectArray-->i).list_together, lt) == 0): i++) ; 384            ! If the contiguous run extends to end of list, the list is now perfect: 385            if (i == MarkedObjectLength) return MarkedObjectArray-->0; 386            ! And otherwise we look to see if any future entries belong in the earlier run: 387            for (l=i+1: l<MarkedObjectLength: l++) 388                if (LT_Compare((MarkedObjectArray-->l).list_together, lt) == 0) { 389                    ! Yes, they do: so we perform a rotation to insert it before element i: 390                    swap = MarkedObjectArray-->l; 391                    for (m=l: m>i: m--) MarkedObjectArray-->m = MarkedObjectArray-->(m-1); 392                    MarkedObjectArray-->i = swap; 393                    ! And now the run is longer: 394                    i++; 395                    if (i == MarkedObjectLength) return MarkedObjectArray-->0; 396                } 397            i--; 398        } 399    } 400    return MarkedObjectArray-->0; 401];

Object Tree Iterator.

Here the raw list is the list of all children of a given parent: since the argument obj is required to be a member of the raw list, we can use parent to find it. Now seeking and advancing are fast, but coalescing is slower.

410Global list_filter_routine; 411 412[ ObjectTreeIterator obj depth required_lt function; 413    if ((obj == nothing) || (parent(obj) == nothing)) return nothing; 414    if (function == START_ITF) obj = child(parent(obj)); 415    if (function == COALESCE_ITF) return ObjectTreeCoalesce(obj); 416    if (function == ADVANCE_ITF) obj = sibling(obj); 417    if (required_lt == 0) required_lt = EMPTY_TEXT_VALUE; 418    for (:: obj = sibling(obj)) { 419        if (obj == nothing) return nothing; 420!if (function == ADVANCE_ITF) print "Considering ", (the) obj, ": ", (TEXT_TY_Say) obj.list_together, ": ", (TEXT_TY_Say) required_lt, ": ", ": ", (TEXT_TY_Say) lt_value, ": ", LT_Compare(obj.list_together, required_lt), "^"; 421        if ((LT_Compare(required_lt, EMPTY_TEXT_VALUE) ~= 0) && 422            (LT_Compare(obj.list_together, required_lt) ~= 0)) continue; 423        if ((c_style & WORKFLAG_BIT) && (depth==0) && (obj hasnt workflag)) continue; 424        if (obj hasnt list_filter_permits) continue; 425        if ((c_style & CONCEAL_BIT) && (ConcealedFromLists(obj))) continue; 426        return obj; 427    } 428];

Coalesce Object Tree.

Again, the return value is the new first entry in the raw list.

434[ ObjectTreeCoalesce obj memb lt later; 435    #Ifdef DBLW; print "^^Sorting out: "; DiagnoseSortList(obj); #Endif; 436    .StartAgain; 437    for (memb=obj: memb~=nothing: memb=sibling(memb)) { 438        lt = memb.list_together; 439        if (LT_Compare(lt, EMPTY_TEXT_VALUE) ~= 0) { 440            ! Find first object in list after contiguous run with this list_together value: 441            for (memb=sibling(memb): 442                (memb) && (LT_Compare(memb.list_together, lt) == 0): memb = sibling(memb)) ; 443            ! If the contiguous run extends to end of list, the list is now perfect: 444            if (memb == 0) return obj; 445            ! And otherwise we look to see if any future entries belong in the earlier run: 446            for (later=sibling(memb): later: later=sibling(later)) 447                if (LT_Compare(later.list_together, lt) == 0) { 448                    ! Yes, they do: so we perform a regrouping of the list and start again: 449                    obj = GroupChildren(parent(obj), lt); 450                    #Ifdef DBLW; print "^^Sorted to: "; DiagnoseSortList(obj); #Endif; 451                    jump StartAgain; 452                } 453        } 454    } 455    return obj; 456]; 457#Ifdef DBLW; 458[ DiagnoseSortList obj memb; 459    for (memb=child(obj): memb~=nothing: memb=sibling(memb)) print memb, " --> "; new_line; 460]; 461#Endif;

WriteListFrom.

And here we go at last. Or at any rate we initialise the quartet of global variables detailing the current list-writing process, and begin.

468[ WriteListFrom first style depth noactivity iter a ol; 469    @push c_iterator; @push c_style; @push c_depth; @push c_margin; 470    if (iter) c_iterator = iter; else c_iterator = ObjectTreeIterator; 471    c_style = style; c_depth = depth; 472    c_margin = 0; if (style & EXTRAINDENT_BIT) c_margin = 1; 473 474    objectloop (a ofclass Object) { 475        give a list_filter_permits; 476        if ((list_filter_routine) && (list_filter_routine(a) == false)) 477            give a ~list_filter_permits; 478    } 479 480    first = c_iterator(first, depth, 0, START_ITF); 481    if (first == nothing) { 482        if (style & ISARE_BIT ~= 0) LIST_WRITER_INTERNAL_RM('W'); 483        else LIST_WRITER_INTERNAL_RM('Y'); 484        if (style & NEWLINE_BIT ~= 0) new_line; 485    } else { 486        if ((noactivity) || (iter)) { 487            WriteListR(first, c_depth, true); 488            say__p = 1; 489        } else { 490            objectloop (ol provides list_together) 491                BlkValueCopy(ol.list_together, EMPTY_TEXT_VALUE); 492            CarryOutActivity(LISTING_CONTENTS_ACT, parent(first)); 493        } 494    } 495 496    @pull c_margin; @pull c_depth; @pull c_style; @pull c_iterator; 497];

Standard Contents Listing Rule.

The default for the listing contents activity is to call this rule in its "for" stage: note that this suppresses the use of the activity, to avoid an infinite regress. The activity is used only for the default ObjectTreeIterator, so there is no need to specify which is used.

506[ STANDARD_CONTENTS_LISTING_R; 507    WriteListFrom(child(parameter_value), c_style, c_depth, true); 508];

Partitioning.

Given qualifying objects x1, ..., xj, we partition them into classes of the equivalence relation xi ~ xj if and only

(i) they both have a plural property (not necessarily the same), and (ii) neither will cause the list-maker to recurse downwards to show the objects inside or on top of them, and (iii) if they cause the list-maker to add information about whether they are worn, lighted or open, then it will add the same information about both, and (iv) they are considered identical by the parser, in that they respond to the same syntaxes of name: so that it is impossible to find a TAKE X command such that X matches one and not the other.

The equivalence classes are numbered consecutively upwards from 1 to n, in order of first appearance in the list. For each object xi, partition_classes-> is the number of its equivalence class. For each equivalence class number c, partition_class_sizes->c is the number of objects in this class.

530#Ifdef DBLW; 531Global DBLW_no_classes; Global DBLW_no_objs; 532[ DebugPartition partition_class_sizes partition_classes first depth i k o; 533    print "[Length of list is ", DBLW_no_objs, " with ", k, " plural.]^"; 534    print "[Partitioned into ", DBLW_no_classes, " equivalence classes.]^"; 535    for (i=1: i<=DBLW_no_classes : i++) { 536        print "Class ", i, " has size ", partition_class_sizes->i, "^"; 537    } 538    for (k=0, o=first: k<DBLW_no_objs : k++, o = c_iterator(o, depth, lt_value, ADVANCE_ITF)) { 539        print "Entry ", k, " has class ", partition_classes->k, 540            " represented by ", o, " with L=", o.list_together, "^"; 541    } 542]; 543#Endif;

Partition List.

The following creates the partition_classes and partition_class_sizes accordingly. We return n, the number of classes.

550[ PartitionList first no_objs depth partition_classes partition_class_sizes 551    i k l n m; 552    for (i=0: i<no_objs: i++) partition_classes->i = 0; 553    n = 1; 554    for (i=first, k=0: k<no_objs: i=c_iterator(i, depth, lt_value, ADVANCE_ITF), k++) 555        if (partition_classes->k == 0) { 556            partition_classes->k = n; partition_class_sizes->n = 1; 557            for (l=c_iterator(i, depth, lt_value, ADVANCE_ITF), m=k+1: 558                (l~=0) && (m<no_objs): 559                l=c_iterator(l, depth, lt_value, ADVANCE_ITF), m++) { 560                if ((partition_classes->m == 0) && (ListEqual(i, l))) { 561                    if (partition_class_sizes->n < 255) (partition_class_sizes->n)++; 562                    partition_classes->m = n; 563                } 564            } 565            if (n < 255) n++; 566        } 567    n--; 568    #Ifdef DBLW; 569    DBLW_no_classes = n; DBLW_no_objs = no_objs; 570    DebugPartition(partition_class_sizes, partition_classes, first, depth); 571    #Endif; 572    return n; 573];

Equivalence Relation.

The above algorithm will fail unless ListEqual is indeed reflexive, symmetric and transitive, which ultimately depends on the care with which Identical is implemented, which in turn hangs on the parse_noun properties compiled by NI. But this seems to be sound.

582[ ListEqual o1 o2; 583    if ((o1.plural == 0) || (o2.plural == 0)) rfalse; 584     if (child(o1) ~= 0 && WillRecurs(o1) ~= 0) rfalse; 585    if (child(o2) ~= 0 && WillRecurs(o2) ~= 0) rfalse; 586    if (c_style & (FULLINV_BIT + PARTINV_BIT) ~= 0) { 587        if ((o1 hasnt worn && o2 has worn) || (o2 hasnt worn && o1 has worn)) rfalse; 588        if ((o1 hasnt light && o2 has light) || (o2 hasnt light && o1 has light)) rfalse; 589        if (o1 has container) { 590            if (o2 hasnt container) rfalse; 591            if ((o1 has open && o2 hasnt open) || (o2 has open && o1 hasnt open)) 592                rfalse; 593        } 594        else if (o2 has container) 595            rfalse; 596    } 597    return Identical(o1, o2); 598]; 599 600[ WillRecurs o; 601    if (c_style & ALWAYS_BIT ~= 0) rtrue; 602    if (c_style & RECURSE_BIT == 0) rfalse; 603    if ((o has supporter) || ((o has container) && (o has open or transparent))) rtrue; 604    rfalse; 605];

Grouping.

A "group" is a maximally-sized run of one or more adjacent partition classes in the list whose first members have a common value of list_together which is a routine or string, and which is not equal to lt_value, the current grouping value. (As we see below, it's by setting lt_value that we restrict attention to a particular group: if we reacted to that as a list_together value here, then we would simply go around in circles, and never be able to see the individual objects in the group.)

For instance, suppose we have objects with list_together values as follows, where R1 and R2 are addresses of routines:

coin (R1), coin (R1), box (R2), statuette (0), coin (R1), box (R2)

Then the partition is 1, 1, 2, 3, 1, 2, so that the final output will be something like "three coins, two boxes and a statuette" – with three grouped terms. Here each partition class is the only member in its group, so the number of groups is the same as the number of classes. (The above list is not coalesced, so the R1 values, for instance, are not contiguous: we need to work in general with non-coalesced lists because not every iterator function will be able to coalesce fully.)

But if we have something like this:

coin (R1), Q (R2), W (R2), coin (R1), statuette (0), E (R2), R (R2)

then the partition is 1, 2, 3, 1, 4, 5, 6 and we have six classes in all. But classes 2 and 3 are grouped together, as are classes 5 and 6, so we end up with a list having just four groups: "two coins, the letters Q and W from a Scrabble set, a statuette and the letters E and R from a Scrabble set". (Again, this list has not been fully coalesced: if it had been, it would be reordered coin, coin, Q, W, E, R, statuette, with partition 1, 1, 2, 3, 4, 5, 6, and three groups: "two coins, the letters Q, W, E and R from a Scrabble set and a statuette".)

The reason we do not group together classes with a common non-zero list_together which is not a routine or string is that low values of list_together are used in coalescing the list into a pleasing order (say, moving all of the key-like items together) but not in grouping: see list_together in the Inform Designer's Manual, 4th edition.

We calculate the number of groups by starting with the number of classes and then subtracting one each time two adjacent classes share list_together in the above sense.

653[ NumberOfGroupsInList o no_classes depth partition_classes partition_class_sizes 654    no_groups cl memb k current_lt lt; 655    current_lt = EMPTY_TEXT_VALUE; 656    lt = EMPTY_TEXT_VALUE; 657    no_groups = no_classes; 658    for (cl=1, memb=o, k=0: cl<=no_classes: cl++) { 659        ! Advance to first member of class number cl 660        while (partition_classes->k ~= cl) { 661            k++; memb = c_iterator(memb, depth, lt_value, ADVANCE_ITF); 662        } 663        if (memb) { ! In case of accidents, but should always happen 664            lt = memb.list_together; 665            if ((LT_Compare(lt, lt_value) ~= 0) && 666                (LT_Compare(lt, EMPTY_TEXT_VALUE) ~= 0) && 667                (LT_Compare(lt, current_lt) == 0)) { 668                no_groups--; 669            } 670            current_lt = lt; 671        } 672    } 673     #Ifdef DBLW; print "[There are ", no_groups, " groups.]^"; #Endif; 674    return no_groups; 675]; 676 677[ LT_Compare lt1 lt2; 678    if (lt1 == lt2) return 0; 679    if (lt1 == 0) lt1 = EMPTY_TEXT_VALUE; 680    if (lt2 == 0) lt2 = EMPTY_TEXT_VALUE; 681    if (TEXT_TY_IsSubstituted(lt1) == false) { 682        if (TEXT_TY_IsSubstituted(lt2) == false) return (lt1-->1)-(lt2-->1); 683        return -1; 684    } 685    if (TEXT_TY_IsSubstituted(lt2) == false) { 686        return -1; 687    } 688    return BlkValueCompare(lt1, lt2); 689];

Write List Recursively.

The big one: WriteListR is the heart of the list-writer.

695[ WriteListR o depth from_start 696    partition_classes partition_class_sizes 697    cl memb index k2 l m no_classes q groups_to_do current_lt; 698    if (o == nothing) return; ! An empty list: no output 699 700    if (from_start) { 701        o = c_iterator(o, depth, 0, COALESCE_ITF); ! Coalesce list and choose new start 702    } 703    o = c_iterator(o, depth, 0, SEEK_ITF); ! Find first entry in list from o 704    if (o == nothing) return; 705 706    ! Count index = length of list 707    for (memb=o, index=0: memb: memb=c_iterator(memb, depth, lt_value, ADVANCE_ITF)) index++; 708 709    if (c_style & ISARE_BIT ~= 0) { 710        SetLWI(index, -1, o); 711        LIST_WRITER_INTERNAL_RM('V', o); 712        if (c_style & NEWLINE_BIT ~= 0) print ":^"; 713        else print (char) ' '; 714        c_style = c_style - ISARE_BIT; 715    } 716 717    partition_classes = RequisitionStack(index/WORDSIZE + 2); 718    partition_class_sizes = RequisitionStack(index/WORDSIZE + 2); 719    if ((partition_classes == 0) || (partition_class_sizes == 0)) 720        return RunTimeProblem(RTP_LISTWRITERMEMORY); 721 722    no_classes = 723        PartitionList(o, index, depth, partition_classes, partition_class_sizes); 724 725    groups_to_do = 726        NumberOfGroupsInList(o, no_classes, depth, partition_classes, partition_class_sizes); 727 728    for (cl=1, memb=o, index=0, current_lt=EMPTY_TEXT_VALUE: groups_to_do>0: cl++) { 729        ! Set memb to first object of partition class cl 730        while (partition_classes->index ~= cl) { 731            index++; memb=c_iterator(memb, depth, lt_value, ADVANCE_ITF); 732            if (memb==0) { print "*** Error in list-writer ***^"; return; } 733        } 734 735        #Ifdef DBLW; 736        ! DebugPartition(partition_class_sizes, partition_classes, o, depth); 737        print "^[Class ", cl, " of ", no_classes, ": first object ", memb, 738            " (", memb.list_together, "); groups_to_do ", groups_to_do, ", 739            current_lt=", current_lt, " listing_size=", listing_size, 740            " lt_value=", lt_value, " memb.list_together=", memb.list_together, "]^"; 741        #Endif; 742 743        if ((LT_Compare(memb.list_together, lt_value) == 0) || 744            (LT_Compare(memb.list_together, EMPTY_TEXT_VALUE) == 0)) current_lt = EMPTY_TEXT_VALUE; 745        else { 746            if (LT_Compare(memb.list_together, current_lt) == 0) continue; 747             748            ! Otherwise this class begins a new group 749            @push listing_size; 750            q = memb; listing_size = 1; l = index; m = cl; 751            while (m < no_classes && 752                (LT_Compare(q.list_together, memb.list_together) == 0)) { 753                m++; 754                while (partition_classes->l ~= m) { 755                    l++; q = c_iterator(q, depth, lt_value, ADVANCE_ITF); 756                } 757                if (LT_Compare(q.list_together, memb.list_together) == 0) 758                    listing_size++; 759            } 760 761            if (listing_size > 1) { 762                ! The new group contains more than one partition class 763                WriteMultiClassGroup(cl, memb, depth, partition_class_sizes); 764                current_lt = memb.list_together; 765                jump GroupComplete; 766            } 767            current_lt = EMPTY_TEXT_VALUE; 768            @pull listing_size; 769        } 770 771        WriteSingleClassGroup(cl, memb, depth, partition_class_sizes->cl); 772 773        .GroupComplete; 774        groups_to_do--; 775        if (c_style & ENGLISH_BIT ~= 0) { 776            if (groups_to_do == 1) { 777                #ifdef SERIAL_COMMA; if (cl > 1) print ","; #endif; 778               LIST_WRITER_INTERNAL_RM('C'); 779            } 780            if (groups_to_do > 1) print ", "; 781        } 782    } 783 784    FreeStack(partition_class_sizes); 785    FreeStack(partition_classes); 786]; ! end of WriteListR

Write Multiple Class Group.

The text of a single group which contains more than one partition class. We carry out the "grouping together" activity, so that the user can add text fore and aft – this is how groups of objects such as "X, Y and Z" can be fluffed up to "the letters X, Y and Z from a Scrabble set" – but the default is simple: we print the grouped items by calling WriteListR once again, but this time starting from X, and where lt_value is set to the common list_together value of X, Y and Z. (That restricts the list to just the objects in this group.) Because lt_value is set to this value, the grouping code won't then group X, Y and Z again, and they will instead be individual classes in the new list – so each will end up being sent, in turn, to WriteSingleClassGroup below, and that is where they are printed.

803[ WriteMultiClassGroup cl memb depth partition_class_sizes pv q k2 l; 804    ! Save the style, because the activity below is allowed to change it 805    q = c_style; 806    if (c_style & INDENT_BIT ~= 0) PrintSpaces(2*(depth+c_margin)); 807 808    BeginActivity(GROUPING_TOGETHER_ACT, memb); 809 810    if (ForActivity(GROUPING_TOGETHER_ACT, memb)) { 811        c_style = c_style &~ NEWLINE_BIT; 812    } else { 813        pv = memb.list_together; 814        if (TEXT_TY_IsSubstituted(pv) == false) { 815            inventory_stage = 1; 816            parser_one = memb; parser_two = depth + c_margin; 817            if ((pv-->1)() == 1) jump Omit__Sublist2; 818        } else if (pv) { 819            ! Set k2 to the number of objects covered by the group 820            k2 = 0; 821            for (l=0 : l<listing_size : l++) k2 = k2 + partition_class_sizes->(l+cl); 822            EnglishNumber(k2); print " "; 823            print (TEXT_TY_Say) pv; 824            if (c_style & ENGLISH_BIT ~= 0) print " ("; 825            if (c_style & INDENT_BIT ~= 0) print ":^"; 826        } 827 828        c_margin++; 829        @push lt_value; @push listing_together; @push listing_size; 830         831        lt_value = memb.list_together; listing_together = memb; 832        #Ifdef DBLW; print "^^DOWN lt_value = ", lt_value, " listing_together = ", memb, "^^"; 833        @push DBLW_no_classes; @push DBLW_no_objs; #Endif; 834        WriteListR(memb, depth, false); 835        #Ifdef DBLW; print "^^UP^^"; @pull DBLW_no_objs; @pull DBLW_no_classes; #Endif; 836 837        @pull listing_size; @pull listing_together; @pull lt_value; 838        c_margin--; 839         840        pv = memb.list_together; 841        if (TEXT_TY_IsSubstituted(pv) == false) { 842            inventory_stage = 2; 843            parser_one = memb; parser_two = depth+c_margin; 844            (pv-->1)(); 845        } else if (LT_Compare(pv, EMPTY_TEXT_VALUE) ~= 0) { 846            if (q & ENGLISH_BIT ~= 0) print ")"; 847        } 848        .Omit__Sublist2; 849    } 850   851    EndActivity(GROUPING_TOGETHER_ACT, memb); 852 853    ! If the NEWLINE_BIT has been forced by the activity, act now 854    ! before it vanishes... 855    if (q & NEWLINE_BIT ~= 0 && c_style & NEWLINE_BIT == 0) new_line; 856 857    ! ...when the original style is restored again: 858    c_style = q; 859];

Write Single Class Group.

The text of a single group which contains exactly one partition class. Because of the way the multiple-class case recurses, every class ends up in this routine sooner or later; this is the place where the actual name of an object is printed, at long last.

868[ WriteSingleClassGroup cl memb depth size q; 869    q = c_style; 870    if (c_style & INDENT_BIT) PrintSpaces(2*(depth+c_margin)); 871    if (size == 1) { 872        if (c_style & NOARTICLE_BIT ~= 0) print (name) memb; 873        else { 874            if (c_style & DEFART_BIT) { 875                if ((cl == 1) && (c_style & CFIRSTART_BIT)) print (The) memb; 876                else print (the) memb; 877            } else { 878                if ((cl == 1) && (c_style & CFIRSTART_BIT)) print (CIndefArt) memb; 879                else print (a) memb; 880            } 881        } 882    } else { 883        if (c_style & DEFART_BIT) { 884            if ((cl == 1) && (c_style & CFIRSTART_BIT)) PrefaceByArticle(memb, 0, size); 885            else PrefaceByArticle(memb, 1, size); 886        } 887        @push listing_size; listing_size = size; 888        CarryOutActivity(PRINTING_A_NUMBER_OF_ACT, memb); 889        @pull listing_size; 890    } 891    if ((size > 1) && (memb hasnt pluralname)) { 892        give memb pluralname; 893        WriteAfterEntry(memb, depth); 894        give memb ~pluralname; 895    } else WriteAfterEntry(memb, depth); 896    c_style = q; 897];

Write After Entry.

Each entry can be followed by supplementary, usually parenthetical, information: exactly what, depends on the style. The extreme case is when the style, and the object, call for recursion to list the object-tree contents: this is achieved by calling WriteListR, using the ObjectTreeIterator (whatever the iterator used at the top level) and increasing the depth by 1.

907[ WriteAfterEntry o depth 908    p recurse_flag parenth_flag eldest_child child_count combo; 909 910    inventory_stage = 2; 911    if (c_style & PARTINV_BIT) { 912        BeginActivity(PRINTING_ROOM_DESC_DETAILS_ACT, o); 913        if (ForActivity(PRINTING_ROOM_DESC_DETAILS_ACT, o) == false) { 914            combo = 0; 915            if (o has light && location hasnt light) combo=combo+1; 916            if (o has container && o hasnt open) combo=combo+2; 917            if ((o has container && (o has open || o has transparent)) 918                && (child(o)==0)) combo=combo+4; 919            if (combo) LIST_WRITER_INTERNAL_RM('A'); ! space and open bracket 920            switch (combo) { 921                1: LIST_WRITER_INTERNAL_RM('D', o); 922                2: LIST_WRITER_INTERNAL_RM('E', o); 923                3: LIST_WRITER_INTERNAL_RM('H', o); 924                4: LIST_WRITER_INTERNAL_RM('F', o); 925                5: LIST_WRITER_INTERNAL_RM('I', o); 926                6: LIST_WRITER_INTERNAL_RM('G', o); 927                7: LIST_WRITER_INTERNAL_RM('J', o); 928            } 929            if (combo) LIST_WRITER_INTERNAL_RM('B'); ! close bracket 930        } 931        EndActivity(PRINTING_ROOM_DESC_DETAILS_ACT, o); 932    } ! end of PARTINV_BIT processing 933 934    if (c_style & FULLINV_BIT) { 935        BeginActivity(PRINTING_INVENTORY_DETAILS_ACT, o); 936        if (ForActivity(PRINTING_INVENTORY_DETAILS_ACT, o) == false) { 937            if (o has light && o has worn) { LIST_WRITER_INTERNAL_RM('A'); LIST_WRITER_INTERNAL_RM('K', o); parenth_flag = true; } 938            else { 939                if (o has light) { LIST_WRITER_INTERNAL_RM('A'); LIST_WRITER_INTERNAL_RM('D', o); parenth_flag = true; } 940                if (o has worn) { LIST_WRITER_INTERNAL_RM('A'); LIST_WRITER_INTERNAL_RM('L', o); parenth_flag = true; } 941            } 942     943            if (o has container) 944                if (o has openable) { 945                    if (parenth_flag) { 946                        #Ifdef SERIAL_COMMA; print ","; #Endif; 947                        LIST_WRITER_INTERNAL_RM('C'); 948                    } else LIST_WRITER_INTERNAL_RM('A', o); 949                    if (o has open) 950                        if (child(o)) LIST_WRITER_INTERNAL_RM('M', o); 951                        else LIST_WRITER_INTERNAL_RM('N', o); 952                    else 953                        if (o has lockable && o has locked) LIST_WRITER_INTERNAL_RM('P', o); 954                        else LIST_WRITER_INTERNAL_RM('O', o); 955                    parenth_flag = true; 956                } 957                else 958                    if (child(o)==0 && o has transparent) 959                        if (parenth_flag) { LIST_WRITER_INTERNAL_RM('C'); LIST_WRITER_INTERNAL_RM('F'); } 960                        else { LIST_WRITER_INTERNAL_RM('A'); LIST_WRITER_INTERNAL_RM('F'); LIST_WRITER_INTERNAL_RM('B'); } 961     962            if (parenth_flag) LIST_WRITER_INTERNAL_RM('B'); 963        } 964        EndActivity(PRINTING_INVENTORY_DETAILS_ACT, o); 965    } ! end of FULLINV_BIT processing 966 967    child_count = 0; 968    eldest_child = nothing; 969    objectloop (p in o) 970        if ((c_style & CONCEAL_BIT == 0) || (ConcealedFromLists(p) == false)) 971            if (p has list_filter_permits) { 972                child_count++; 973                if (eldest_child == nothing) eldest_child = p; 974            } 975 976    if (child_count && (c_style & ALWAYS_BIT)) { 977        if (c_style & ENGLISH_BIT) { print " "; LIST_WRITER_INTERNAL_RM('Q', o); print " "; } 978        recurse_flag = true; 979    } 980     981    if (child_count && (c_style & RECURSE_BIT)) { 982        if (o has supporter) { 983            if (c_style & ENGLISH_BIT) { 984                if (c_style & TERSE_BIT) { 985                    LIST_WRITER_INTERNAL_RM('A', o); 986                    LIST_WRITER_INTERNAL_RM('R', o); 987                } else LIST_WRITER_INTERNAL_RM('S', o); 988            } 989            recurse_flag = true; 990        } 991        if (o has container && (o has open || o has transparent)) { 992            if (c_style & ENGLISH_BIT) { 993                if (c_style & TERSE_BIT) { 994                    LIST_WRITER_INTERNAL_RM('A', o); 995                    LIST_WRITER_INTERNAL_RM('T', o); 996                } else LIST_WRITER_INTERNAL_RM('U', o); 997            } 998            recurse_flag = true; 999        } 1000    } 1001 1002    if (recurse_flag && (c_style & ENGLISH_BIT)) { 1003        SetLWI(child_count, -1, eldest_child); 1004        LIST_WRITER_INTERNAL_RM('V', o); print " "; 1005    } 1006 1007    if (c_style & NEWLINE_BIT) new_line; 1008 1009    if (recurse_flag) { 1010        o = child(o); 1011        @push lt_value; @push listing_together; @push listing_size; 1012        @push c_iterator; 1013        c_iterator = ObjectTreeIterator; 1014        lt_value = EMPTY_TEXT_VALUE; listing_together = 0; listing_size = 0; 1015        WriteListR(o, depth+1, true); 1016        @pull c_iterator; 1017        @pull listing_size; @pull listing_together; @pull lt_value; 1018        if (c_style & TERSE_BIT) LIST_WRITER_INTERNAL_RM('B'); 1019    } 1020];

Internal Rule.

This rule does nothing in itself; it exists as a placeholder for the response texts used by the list-writer.

1027[ LIST_WRITER_INTERNAL_R; 1028];