1 # A doubly linked list permits bidirectional traversal.
  2 
  3 container duplex-list:_elem [
  4   value:_elem
  5   next:&:duplex-list:_elem
  6   prev:&:duplex-list:_elem
  7 ]
  8 
  9 # should I say in/contained-in:result, allow ingredients to refer to products?
 10 def push x:_elem, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [
 11   local-scope
 12   load-ingredients
 13   result:&:duplex-list:_elem <- new {(duplex-list _elem): type}
 14   *result <- merge x, in, 0
 15   {
 16   ¦ break-unless in
 17   ¦ *in <- put *in, prev:offset, result
 18   }
 19   return result  # needed explicitly because we need to replace 'in' with 'result'
 20 ]
 21 
 22 def first in:&:duplex-list:_elem -> result:_elem [
 23   local-scope
 24   load-ingredients
 25   return-unless in, 0
 26   result <- get *in, value:offset
 27 ]
 28 
 29 def next in:&:duplex-list:_elem -> result:&:duplex-list:_elem/contained-in:in [
 30   local-scope
 31   load-ingredients
 32   return-unless in, 0
 33   result <- get *in, next:offset
 34 ]
 35 
 36 def prev in:&:duplex-list:_elem -> result:&:duplex-list:_elem/contained-in:in [
 37   local-scope
 38   load-ingredients
 39   return-unless in, 0
 40   result <- get *in, prev:offset
 41   return result
 42 ]
 43 
 44 scenario duplex-list-handling [
 45   run [
 46   ¦ local-scope
 47   ¦ # reserve locations 0-9 to check for missing null check
 48   ¦ 10:num/raw <- copy 34
 49   ¦ 11:num/raw <- copy 35
 50   ¦ list:&:duplex-list:num <- push 3, 0
 51   ¦ list <- push 4, list
 52   ¦ list <- push 5, list
 53   ¦ list2:&:duplex-list:num <- copy list
 54   ¦ 20:num/raw <- first list2
 55   ¦ list2 <- next list2
 56   ¦ 21:num/raw <- first list2
 57   ¦ list2 <- next list2
 58   ¦ 22:num/raw <- first list2
 59   ¦ 30:&:duplex-list:num/raw <- next list2
 60   ¦ 31:num/raw <- first 30:&:duplex-list:num/raw
 61   ¦ 32:&:duplex-list:num/raw <- next 30:&:duplex-list:num/raw
 62   ¦ 33:&:duplex-list:num/raw <- prev 30:&:duplex-list:num/raw
 63   ¦ list2 <- prev list2
 64   ¦ 40:num/raw <- first list2
 65   ¦ list2 <- prev list2
 66   ¦ 41:num/raw <- first list2
 67   ¦ 50:bool/raw <- equal list, list2
 68   ]
 69   memory-should-contain [
 70   ¦ 0 <- 0  # no modifications to null pointers
 71   ¦ 10 <- 34
 72   ¦ 11 <- 35
 73   ¦ 20 <- 5  # scanning next
 74   ¦ 21 <- 4
 75   ¦ 22 <- 3
 76   ¦ 30 <- 0  # null
 77   ¦ 31 <- 0  # first of null
 78   ¦ 32 <- 0  # next of null
 79   ¦ 33 <- 0  # prev of null
 80   ¦ 40 <- 4  # then start scanning prev
 81   ¦ 41 <- 5
 82   ¦ 50 <- 1  # list back at start
 83   ]
 84 ]
 85 
 86 def length l:&:duplex-list:_elem -> result:num [
 87   local-scope
 88   load-ingredients
 89   result <- copy 0
 90   {
 91   ¦ break-unless l
 92   ¦ result <- add result, 1
 93   ¦ l <- next l
 94   ¦ loop
 95   }
 96 ]
 97 
 98 # insert 'x' after 'in'
 99 def insert x:_elem, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [
100   local-scope
101   load-ingredients
102   new-node:&:duplex-list:_elem <- new {(duplex-list _elem): type}
103   *new-node <- put *new-node, value:offset, x
104   # save old next before changing it
105   next-node:&:duplex-list:_elem <- get *in, next:offset
106   *in <- put *in, next:offset, new-node
107   *new-node <- put *new-node, prev:offset, in
108   *new-node <- put *new-node, next:offset, next-node
109   return-unless next-node
110   *next-node <- put *next-node, prev:offset, new-node
111 ]
112 
113 scenario inserting-into-duplex-list [
114   local-scope
115   list:&:duplex-list:num <- push 3, 0
116   list <- push 4, list
117   list <- push 5, list
118   run [
119   ¦ list2:&:duplex-list:num <- next list  # inside list
120   ¦ list2 <- insert 6, list2
121   ¦ # check structure like before
122   ¦ list2 <- copy list
123   ¦ 10:num/raw <- first list2
124   ¦ list2 <- next list2
125   ¦ 11:num/raw <- first list2
126   ¦ list2 <- next list2
127   ¦ 12:num/raw <- first list2
128   ¦ list2 <- next list2
129   ¦ 13:num/raw <- first list2
130   ¦ list2 <- prev list2
131   ¦ 20:num/raw <- first list2
132   ¦ list2 <- prev list2
133   ¦ 21:num/raw <- first list2
134   ¦ list2 <- prev list2
135   ¦ 22:num/raw <- first list2
136   ¦ 30:bool/raw <- equal list, list2
137   ]
138   memory-should-contain [
139   ¦ 10 <- 5  # scanning next
140   ¦ 11 <- 4
141   ¦ 12 <- 6  # inserted element
142   ¦ 13 <- 3
143   ¦ 20 <- 6  # then prev
144   ¦ 21 <- 4
145   ¦ 22 <- 5
146   ¦ 30 <- 1  # list back at start
147   ]
148 ]
149 
150 scenario inserting-at-end-of-duplex-list [
151   local-scope
152   list:&:duplex-list:num <- push 3, 0
153   list <- push 4, list
154   list <- push 5, list
155   run [
156   ¦ list2:&:duplex-list:num <- next list  # inside list
157   ¦ list2 <- next list2  # now at end of list
158   ¦ list2 <- insert 6, list2
159   ¦ # check structure like before
160   ¦ list2 <- copy list
161   ¦ 10:num/raw <- first list2
162   ¦ list2 <- next list2
163   ¦ 11:num/raw <- first list2
164   ¦ list2 <- next list2
165   ¦ 12:num/raw <- first list2
166   ¦ list2 <- next list2
167   ¦ 13:num/raw <- first list2
168   ¦ list2 <- prev list2
169   ¦ 20:num/raw <- first list2
170   ¦ list2 <- prev list2
171   ¦ 21:num/raw <- first list2
172   ¦ list2 <- prev list2
173   ¦ 22:num/raw <- first list2
174   ¦ 30:bool/raw <- equal list, list2
175   ]
176   memory-should-contain [
177   ¦ 10 <- 5  # scanning next
178   ¦ 11 <- 4
179   ¦ 12 <- 3
180   ¦ 13 <- 6  # inserted element
181   ¦ 20 <- 3  # then prev
182   ¦ 21 <- 4
183   ¦ 22 <- 5
184   ¦ 30 <- 1  # list back at start
185   ]
186 ]
187 
188 scenario inserting-after-start-of-duplex-list [
189   local-scope
190   list:&:duplex-list:num <- push 3, 0
191   list <- push 4, list
192   list <- push 5, list
193   run [
194   ¦ list <- insert 6, list
195   ¦ # check structure like before
196   ¦ list2:&:duplex-list:num <- copy list
197   ¦ 10:num/raw <- first list2
198   ¦ list2 <- next list2
199   ¦ 11:num/raw <- first list2
200   ¦ list2 <- next list2
201   ¦ 12:num/raw <- first list2
202   ¦ list2 <- next list2
203   ¦ 13:num/raw <- first list2
204   ¦ list2 <- prev list2
205   ¦ 20:num/raw <- first list2
206   ¦ list2 <- prev list2
207   ¦ 21:num/raw <- first list2
208   ¦ list2 <- prev list2
209   ¦ 22:num/raw <- first list2
210   ¦ 30:bool/raw <- equal list, list2
211   ]
212   memory-should-contain [
213   ¦ 10 <- 5  # scanning next
214   ¦ 11 <- 6  # inserted element
215   ¦ 12 <- 4
216   ¦ 13 <- 3
217   ¦ 20 <- 4  # then prev
218   ¦ 21 <- 6
219   ¦ 22 <- 5
220   ¦ 30 <- 1  # list back at start
221   ]
222 ]
223 
224 # remove 'x' from its surrounding list 'in'
225 #
226 # Returns null if and only if list is empty. Beware: in that case any other
227 # pointers to the head are now invalid.
228 def remove x:&:duplex-list:_elem/contained-in:in, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [
229   local-scope
230   load-ingredients
231   # if 'x' is null, return
232   return-unless x
233   next-node:&:duplex-list:_elem <- get *x, next:offset
234   prev-node:&:duplex-list:_elem <- get *x, prev:offset
235   # null x's pointers
236   *x <- put *x, next:offset, 0
237   *x <- put *x, prev:offset, 0
238   # if next-node is not null, set its prev pointer
239   {
240   ¦ break-unless next-node
241   ¦ *next-node <- put *next-node, prev:offset, prev-node
242   }
243   # if prev-node is not null, set its next pointer and return
244   {
245   ¦ break-unless prev-node
246   ¦ *prev-node <- put *prev-node, next:offset, next-node
247   ¦ return
248   }
249   # if prev-node is null, then we removed the head node at 'in'
250   # return the new head rather than the old 'in'
251   return next-node
252 ]
253 
254 scenario removing-from-duplex-list [
255   local-scope
256   list:&:duplex-list:num <- push 3, 0
257   list <- push 4, list
258   list <- push 5, list
259   run [
260   ¦ list2:&:duplex-list:num <- next list  # second element
261   ¦ list <- remove list2, list
262   ¦ 10:bool/raw <- equal list2, 0
263   ¦ # check structure like before
264   ¦ list2 <- copy list
265   ¦ 11:num/raw <- first list2
266   ¦ list2 <- next list2
267   ¦ 12:num/raw <- first list2
268   ¦ 20:&:duplex-list:num/raw <- next list2
269   ¦ list2 <- prev list2
270   ¦ 30:num/raw <- first list2
271   ¦ 40:bool/raw <- equal list, list2
272   ]
273   memory-should-contain [
274   ¦ 10 <- 0  # remove returned non-null
275   ¦ 11 <- 5  # scanning next, skipping deleted element
276   ¦ 12 <- 3
277   ¦ 20 <- 0  # no more elements
278   ¦ 30 <- 5  # prev of final element
279   ¦ 40 <- 1  # list back at start
280   ]
281 ]
282 
283 scenario removing-from-start-of-duplex-list [
284   local-scope
285   list:&:duplex-list:num <- push 3, 0
286   list <- push 4, list
287   list <- push 5, list
288   run [
289   ¦ list <- remove list, list
290   ¦ # check structure like before
291   ¦ list2:&:duplex-list:num <- copy list
292   ¦ 10:num/raw <- first list2
293   ¦ list2 <- next list2
294   ¦ 11:num/raw <- first list2
295   ¦ 20:&:duplex-list:num/raw <- next list2
296   ¦ list2 <- prev list2
297   ¦ 30:num/raw <- first list2
298   ¦ 40:bool/raw <- equal list, list2
299   ]
300   memory-should-contain [
301   ¦ 10 <- 4  # scanning next, skipping deleted element
302   ¦ 11 <- 3
303   ¦ 20 <- 0  # no more elements
304   ¦ 30 <- 4  # prev of final element
305   ¦ 40 <- 1  # list back at start
306   ]
307 ]
308 
309 scenario removing-from-end-of-duplex-list [
310   local-scope
311   list:&:duplex-list:num <- push 3, 0
312   list <- push 4, list
313   list <- push 5, list
314   run [
315   ¦ # delete last element
316   ¦ list2:&:duplex-list:num <- next list
317   ¦ list2 <- next list2
318   ¦ list <- remove list2, list
319   ¦ 10:bool/raw <- equal list2, 0
320   ¦ # check structure like before
321   ¦ list2 <- copy list
322   ¦ 11:num/raw <- first list2
323   ¦ list2 <- next list2
324   ¦ 12:num/raw <- first list2
325   ¦ 20:&:duplex-list:num/raw <- next list2
326   ¦ list2 <- prev list2
327   ¦ 30:num/raw <- first list2
328   ¦ 40:bool/raw <- equal list, list2
329   ]
330   memory-should-contain [
331   ¦ 10 <- 0  # remove returned non-null
332   ¦ 11 <- 5  # scanning next, skipping deleted element
333   ¦ 12 <- 4
334   ¦ 20 <- 0  # no more elements
335   ¦ 30 <- 5  # prev of final element
336   ¦ 40 <- 1  # list back at start
337   ]
338 ]
339 
340 scenario removing-from-singleton-duplex-list [
341   local-scope
342   list:&:duplex-list:num <- push 3, 0
343   run [
344   ¦ list <- remove list, list
345   ¦ 1:num/raw <- copy list
346   ]
347   memory-should-contain [
348   ¦ 1 <- 0  # back to an empty list
349   ]
350 ]
351 
352 def remove x:&:duplex-list:_elem/contained-in:in, n:num, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [
353   local-scope
354   load-ingredients
355   i:num <- copy 0
356   curr:&:duplex-list:_elem <- copy x
357   {
358   ¦ done?:bool <- greater-or-equal i, n
359   ¦ break-if done?
360   ¦ break-unless curr
361   ¦ next:&:duplex-list:_elem <- next curr
362   ¦ in <- remove curr, in
363   ¦ curr <- copy next
364   ¦ i <- add i, 1
365   ¦ loop
366   }
367 ]
368 
369 scenario removing-multiple-from-duplex-list [
370   local-scope
371   list:&:duplex-list:num <- push 3, 0
372   list <- push 4, list
373   list <- push 5, list
374   run [
375   ¦ list2:&:duplex-list:num <- next list  # second element
376   ¦ list <- remove list2, 2, list
377   ¦ stash list
378   ]
379   trace-should-contain [
380   ¦ app: 5
381   ]
382 ]
383 
384 # remove values between 'start' and 'end' (both exclusive).
385 # also clear pointers back out from start/end for hygiene.
386 # set end to 0 to delete everything past start.
387 # can't set start to 0 to delete everything before end, because there's no
388 # clean way to return the new head pointer.
389 def remove-between start:&:duplex-list:_elem, end:&:duplex-list:_elem/contained-in:start -> start:&:duplex-list:_elem [
390   local-scope
391   load-ingredients
392   next:&:duplex-list:_elem <- get *start, next:offset
393   nothing-to-delete?:bool <- equal next, end
394   return-if nothing-to-delete?
395   assert next, [malformed duplex list]
396   # start->next->prev = 0
397   # start->next = end
398   *next <- put *next, prev:offset, 0
399   *start <- put *start, next:offset, end
400   return-unless end
401   # end->prev->next = 0
402   # end->prev = start
403   prev:&:duplex-list:_elem <- get *end, prev:offset
404   assert prev, [malformed duplex list - 2]
405   *prev <- put *prev, next:offset, 0
406   *end <- put *end, prev:offset, start
407 ]
408 
409 scenario remove-range [
410   # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
411   local-scope
412   list:&:duplex-list:num <- push 18, 0
413   list <- push 17, list
414   list <- push 16, list
415   list <- push 15, list
416   list <- push 14, list
417   list <- push 13, list
418   run [
419   ¦ # delete 16 onwards
420   ¦ # first pointer: to the third element
421   ¦ list2:&:duplex-list:num <- next list
422   ¦ list2 <- next list2
423   ¦ list2 <- remove-between list2, 0
424   ¦ # now check the list
425   ¦ 10:num/raw <- get *list, value:offset
426   ¦ list <- next list
427   ¦ 11:num/raw <- get *list, value:offset
428   ¦ list <- next list
429   ¦ 12:num/raw <- get *list, value:offset
430   ¦ 20:&:duplex-list:num/raw <- next list
431   ]
432   memory-should-contain [
433   ¦ 10 <- 13
434   ¦ 11 <- 14
435   ¦ 12 <- 15
436   ¦ 20 <- 0
437   ]
438 ]
439 
440 scenario remove-range-to-final [
441   local-scope
442   # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
443   list:&:duplex-list:num <- push 18, 0
444   list <- push 17, list
445   list <- push 16, list
446   list <- push 15, list
447   list <- push 14, list
448   list <- push 13, list
449   run [
450   ¦ # delete 15, 16 and 17
451   ¦ # start pointer: to the second element
452   ¦ list2:&:duplex-list:num <- next list
453   ¦ # end pointer: to the last (sixth) element
454   ¦ end:&:duplex-list:num <- next list2
455   ¦ end <- next end
456   ¦ end <- next end
457   ¦ end <- next end
458   ¦ remove-between list2, end
459   ¦ # now check the list
460   ¦ 10:num/raw <- get *list, value:offset
461   ¦ list <- next list
462   ¦ 11:num/raw <- get *list, value:offset
463   ¦ list <- next list
464   ¦ 12:num/raw <- get *list, value:offset
465   ¦ 20:&:duplex-list:num/raw <- next list
466   ]
467   memory-should-contain [
468   ¦ 10 <- 13
469   ¦ 11 <- 14
470   ¦ 12 <- 18
471   ¦ 20 <- 0  # no more elements
472   ]
473 ]
474 
475 scenario remove-range-empty [
476   local-scope
477   # construct a duplex list with three elements [13, 14, 15]
478   list:&:duplex-list:num <- push 15, 0
479   list <- push 14, list
480   list <- push 13, list
481   run [
482   ¦ # delete between first and second element (i.e. nothing)
483   ¦ list2:&:duplex-list:num <- next list
484   ¦ remove-between list, list2
485   ¦ # now check the list
486   ¦ 10:num/raw <- get *list, value:offset
487   ¦ list <- next list
488   ¦ 11:num/raw <- get *list, value:offset
489   ¦ list <- next list
490   ¦ 12:num/raw <- get *list, value:offset
491   ¦ 20:&:duplex-list:num/raw <- next list
492   ]
493   # no change
494   memory-should-contain [
495   ¦ 10 <- 13
496   ¦ 11 <- 14
497   ¦ 12 <- 15
498   ¦ 20 <- 0
499   ]
500 ]
501 
502 scenario remove-range-to-end [
503   local-scope
504   # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
505   list:&:duplex-list:num <- push 18, 0
506   list <- push 17, list
507   list <- push 16, list
508   list <- push 15, list
509   list <- push 14, list
510   list <- push 13, list
511   run [
512   ¦ # remove the third element and beyond
513   ¦ list2:&:duplex-list:num <- next list
514   ¦ remove-between list2, 0
515   ¦ # now check the list
516   ¦ 10:num/raw <- get *list, value:offset
517   ¦ list <- next list
518   ¦ 11:num/raw <- get *list, value:offset
519   ¦ 20:&:duplex-list:num/raw <- next list
520   ]
521   memory-should-contain [
522   ¦ 10 <- 13
523   ¦ 11 <- 14
524   ¦ 20 <- 0
525   ]
526 ]
527 
528 # insert list beginning at 'start' after 'in'
529 def splice in:&:duplex-list:_elem, start:&:duplex-list:_elem/contained-in:in -> in:&:duplex-list:_elem [
530   local-scope
531   load-ingredients
532   return-unless in
533   return-unless start
534   end:&:duplex-list:_elem <- last start
535   next:&:duplex-list:_elem <- next in
536   {
537   ¦ break-unless next
538   ¦ *end <- put *end, next:offset, next
539   ¦ *next <- put *next, prev:offset, end
540   }
541   *in <- put *in, next:offset, start
542   *start <- put *start, prev:offset, in
543 ]
544 
545 # insert contents of 'new' after 'in'
546 def insert in:&:duplex-list:_elem, new:&:@:_elem -> in:&:duplex-list:_elem [
547   local-scope
548   load-ingredients
549   return-unless in
550   return-unless new
551   len:num <- length *new
552   return-unless len
553   curr:&:duplex-list:_elem <- copy in
554   idx:num <- copy 0
555   {
556   ¦ done?:bool <- greater-or-equal idx, len
557   ¦ break-if done?
558   ¦ c:_elem <- index *new, idx
559   ¦ insert c, curr
560   ¦ # next iter
561   ¦ curr <- next curr
562   ¦ idx <- add idx, 1
563   ¦ loop
564   }
565 ]
566 
567 def append in:&:duplex-list:_elem, new:&:duplex-list:_elem/contained-in:in -> in:&:duplex-list:_elem [
568   local-scope
569   load-ingredients
570   last:&:duplex-list:_elem <- last in
571   *last <- put *last, next:offset, new
572   return-unless new
573   *new <- put *new, prev:offset, last
574 ]
575 
576 def last in:&:duplex-list:_elem -> result:&:duplex-list:_elem [
577   local-scope
578   load-ingredients
579   result <- copy in
580   {
581   ¦ next:&:duplex-list:_elem <- next result
582   ¦ break-unless next
583   ¦ result <- copy next
584   ¦ loop
585   }
586 ]
587 
588 # does a duplex list start with a certain sequence of elements?
589 def match x:&:duplex-list:_elem, y:&:@:_elem -> result:bool [
590   local-scope
591   load-ingredients
592   i:num <- copy 0
593   max:num <- length *y
594   {
595   ¦ done?:bool <- greater-or-equal i, max
596   ¦ break-if done?
597   ¦ expected:_elem <- index *y, i
598   ¦ return-unless x, 0/no-match
599   ¦ curr:_elem <- first x
600   ¦ curr-matches?:bool <- equal curr, expected
601   ¦ return-unless curr-matches?, 0/no-match
602   ¦ x <- next x
603   ¦ i <- add i, 1
604   ¦ loop
605   }
606   return 1/successful-match
607 ]
608 
609 scenario duplex-list-match [
610   local-scope
611   list:&:duplex-list:char <- push 97/a, 0
612   list <- push 98/b, list
613   list <- push 99/c, list
614   list <- push 100/d, list
615   run [
616   ¦ 10:bool/raw <- match list, []
617   ¦ 11:bool/raw <- match list, [d]
618   ¦ 12:bool/raw <- match list, [dc]
619   ¦ 13:bool/raw <- match list, [dcba]
620   ¦ 14:bool/raw <- match list, [dd]
621   ¦ 15:bool/raw <- match list, [dcbax]
622   ]
623   memory-should-contain [
624   ¦ 10 <- 1  # matches []
625   ¦ 11 <- 1  # matches [d]
626   ¦ 12 <- 1  # matches [dc]
627   ¦ 13 <- 1  # matches [dcba]
628   ¦ 14 <- 0  # does not match [dd]
629   ¦ 15 <- 0  # does not match [dcbax]
630   ]
631 ]
632 
633 # helper for debugging
634 def dump-from x:&:duplex-list:_elem [
635   local-scope
636   load-ingredients
637   $print x, [: ]
638   {
639   ¦ break-unless x
640   ¦ c:_elem <- get *x, value:offset
641   ¦ $print c, [ ]
642   ¦ x <- next x
643   ¦ {
644   ¦ ¦ is-newline?:bool <- equal c, 10/newline
645   ¦ ¦ break-unless is-newline?
646   ¦ ¦ $print 10/newline
647   ¦ ¦ $print x, [: ]
648   ¦ }
649   ¦ loop
650   }
651   $print 10/newline, [---], 10/newline
652 ]
653 
654 scenario stash-duplex-list [
655   local-scope
656   list:&:duplex-list:num <- push 1, 0
657   list <- push 2, list
658   list <- push 3, list
659   run [
660   ¦ stash [list:], list
661   ]
662   trace-should-contain [
663   ¦ app: list: 3 <-> 2 <-> 1
664   ]
665 ]
666 
667 def to-text in:&:duplex-list:_elem -> result:text [
668   local-scope
669   load-ingredients
670   buf:&:buffer:char <- new-buffer 80
671   buf <- to-buffer in, buf
672   result <- buffer-to-array buf
673 ]
674 
675 # variant of 'to-text' which stops printing after a few elements (and so is robust to cycles)
676 def to-text-line in:&:duplex-list:_elem -> result:text [
677   local-scope
678   load-ingredients
679   buf:&:buffer:char <- new-buffer 80
680   buf <- to-buffer in, buf, 6  # max elements to display
681   result <- buffer-to-array buf
682 ]
683 
684 def to-buffer in:&:duplex-list:_elem, buf:&:buffer:char -> buf:&:buffer:char [
685   local-scope
686   load-ingredients
687   {
688   ¦ break-if in
689   ¦ buf <- append buf, [[]]
690   ¦ return
691   }
692   # append in.value to buf
693   val:_elem <- get *in, value:offset
694   buf <- append buf, val
695   # now prepare next
696   next:&:duplex-list:_elem <- next in
697   nextn:num <- copy next
698   return-unless next
699   buf <- append buf, [ <-> ]
700   # and recurse
701   remaining:num, optional-ingredient-found?:bool <- next-ingredient
702   {
703   ¦ break-if optional-ingredient-found?
704   ¦ # unlimited recursion
705   ¦ buf <- to-buffer next, buf
706   ¦ return
707   }
708   {
709   ¦ break-unless remaining
710   ¦ # limited recursion
711   ¦ remaining <- subtract remaining, 1
712   ¦ buf <- to-buffer next, buf, remaining
713   ¦ return
714   }
715   # past recursion depth; insert ellipses and stop
716   append buf, [...]
717 ]
718 
719 scenario stash-empty-duplex-list [
720   local-scope
721   x:&:duplex-list:num <- copy 0
722   run [
723   ¦ stash x
724   ]
725   trace-should-contain [
726   ¦ app: []
727   ]
728 ]