Imported Upstream version 1.64.0
[platform/upstream/boost.git] / libs / context / doc / execution_context_v2.qbk
1 [/
2           Copyright Oliver Kowalke 2014.
3  Distributed under the Boost Software License, Version 1.0.
4     (See accompanying file LICENSE_1_0.txt or copy at
5           http://www.boost.org/LICENSE_1_0.txt
6 ]
7
8 [#ecv2]
9 [section:ecv2 Class execution_context (version 2)]
10
11 [warning __econtext__ is deprecated.]
12
13 [note Include `execution_context.hpp` and pass define BOOST_EXECUTION_CONTEXT=2
14 at compilers command-line in order to use __econtext__ (v2).]
15
16 [note Segmented stacks (['segmented-stacks=on]), e.g. on demand growing stacks,
17 are not supported by __econtext__ (v2).]
18
19 Class __econtext__ encapsulates context switching and manages the associated
20 context' stack (allocation/deallocation).
21
22 __econtext__ allocates the context stack (using its [link stack
23 __stack_allocator__] argument) and creates a control structure on top of it.
24 This structure is responsible for managing context' stack. The address of the
25 control structure is stored in the first frame of context' stack (e.g. it can
26 not directly accessed from within __econtext__). In contrast to __ecv1__ the
27 ownership of the control structure is not shared (no member variable to control
28 structure in __econtext__). __econtext__ keeps internally a state that is moved
29 by a call of __ec_op__ (`*this` will be invalidated), e.g. after a calling
30 __ec_op__, `*this` can not be used for an additional context switch.
31
32 __econtext__ is  only move-constructible and move-assignable.
33
34 The moved state is assigned to a new instance of __econtext__. This object
35 becomes the first argument of the context-function, if the context was resumed
36 the first time, or the first element in a tuple returned by __ec_op__ that has
37 been called in the resumed context.
38 In contrast to __ecv1__, the context switch is faster because no global pointer
39 etc. is involved.
40
41 [important Segmented stacks are not supported by __econtext__ (v2).]
42
43 On return the context-function of the current context has to specify an
44 __econtext__ to which the execution control is transferred after termination
45 of the current context.
46
47 If an instance with valid state goes out of scope and the context-function has
48 not yet returned, the stack is traversed in order to access the control
49 structure (address stored at the first stack frame) and context' stack is
50 deallocated via the __stack_allocator__. The stack walking makes the destruction
51 of __econtext__ slow and should be prevented if possible.
52
53 __econtext__ expects a __context_fn__ with signature
54 `execution_context(execution_context ctx, Args ... args)`. The parameter `ctx`
55 represents the context from which this context was resumed (e.g. that has called
56 __ec_op__ on `*this`) and `args` are the data passed to __ec_op__. The return
57 value represents the execution_context that has to be resumed, after termiantion
58 of this context.
59
60 Benefits of __ecv2__ over __ecv1__ are: faster context switch, type-safety of
61 passed/returned arguments.
62
63
64 [heading usage of __econtext__]
65
66         int n=35;
67         ctx::execution_context<int> source(
68             [n](ctx::execution_context<int> && sink,int) mutable {
69                 int a=0;
70                 int b=1;
71                 while(n-->0){
72                     auto result=sink(a);
73                     sink=std::move(std::get<0>(result));
74                     auto next=a+b;
75                     a=b;
76                     b=next;
77                 }
78                 return std::move(sink);
79             });
80         for(int i=0;i<10;++i){
81             auto result=source(i);
82             source=std::move(std::get<0>(result));
83             std::cout<<std::get<1>(result)<<" ";
84         }
85
86         output:
87             0 1 1 2 3 5 8 13 21 34
88
89 This simple example demonstrates the basic usage of __econtext__ as a generator.
90 The context `sink` represents the ['main]-context (function ['main()] running).
91 `sink` is generated by the framework (first element of lambda's parameter list).
92 Because the state is invalidated (== changed) by each call of __ec_op__, the new
93 state of the __econtext__, returned by __ec_op__, needs to be assigned to `sink`
94 after each call.
95
96 The lambda that calculates the Fibonacci numbers is executed inside
97 the context represented by `source`. Calculated Fibonacci numbers are
98 transferred between the two context' via expression ['sink(a)] (and returned by
99 ['source()]). Note that this example represents a ['generator] thus the value
100 transferred into the lambda via ['source()] is not used. Using
101 ['boost::optional<>] as transferred type, might also appropriate to express this
102 fact.
103
104 The locale variables `a`, `b` and ` next` remain their values during each
105 context switch (['yield(a)]). This is possible due `source` has its own stack
106 and the stack is exchanged by each context switch.
107
108
109 [heading parameter passing]
110 With `execution_context<void>` no data will be transferred, only the context
111 switch is executed.
112
113     boost::context::execution_context<void> ctx1([](boost::context::execution_context<void> && ctx2){
114                 std::printf("inside ctx1\n");
115                 return ctx2();
116             });
117     ctx1();
118
119     output:
120         inside ctx1
121
122 `ctx1()` resumes `ctx1`, e.g. the lambda passed at the constructor of `ctx1` is
123 entered. Argument `ctx2` represents the context that has been suspended with the
124 invocation of `ctx1()`. When the lambda returns `ctx2`, context `ctx1` will be
125 terminated while the context represented by `ctx2` is resumed, hence the control
126 of execution returns from `ctx1()`.
127
128 The arguments passed to __ec_op__, in one context, is passed as the last
129 arguments of the __context_fn__ if the context is started for the first time.
130 In all following invocations of __ec_op__ the arguments passed to __ec_op__, in
131 one context, is returned by __ec_op__ in the other context.
132
133     boost::context::execution_context<int> ctx1([](boost::context::execution_context<int> && ctx2,int j){
134                 std::printf("inside ctx1,j==%d\n",j);
135                 std::tie(ctx2,j)=ctx2(j+1);
136                 return std::move(ctx2);
137             });
138     int i=1;
139     std::tie(ctx1,i)=ctx1(i);
140     std::printf("i==%d\n",i);
141
142     output:
143         inside ctx1,j==1
144         i==2
145
146 `ctx1(i)` enters the lambda in context `ctx1` with argument `j=1`. The
147 expression `ctx2(j+1)` resumes the context represented by `ctx2` and transfers
148 back an integer of `j+1`. On return of `ctx1(i)`, the variable `i` contains the
149 value of `j+1`.
150
151 If more than one argument has to be transferred, the signature of the
152 context-function is simply extended.
153
154     boost::context::execution_context<int,int> ctx1([](boost::context::execution_context<int,int> && ctx2,int i,int j){
155                 std::printf("inside ctx1,i==%d,j==%d\n",i,j);
156                 std::tie(ctx2,i,j)=ctx2(i+j,i-j);
157                 return std::move(ctx2);
158             });
159     int i=2,j=1;
160     std::tie(ctx1,i,j)=ctx1(i,j);
161     std::printf("i==%d,j==%d\n",i,j);
162
163     output:
164         inside ctx1,i==2,j==1
165         i==3,j==1
166
167 For use-cases, that require to transfer data of different type in each
168 direction, ['boost::variant<>] could be used.
169
170         class X{
171         private:
172             std::exception_ptr excptr_;
173             boost::context::execution_context<boost::variant<int,std::string>> ctx_;
174
175         public:
176             X():
177                 excptr_(),
178                 ctx_([=](boost::context::execution_context<boost::variant<int,std::string>> && ctx,boost::variant<int,std::string> data){
179                         try {
180                             for (;;) {
181                                 int i=boost::get<int>(data);
182                                 data=boost::lexical_cast<std::string>(i);
183                                 auto result=ctx(data);
184                                 ctx=std::move(std::get<0>(result));
185                                 data=std::get<1>(result);
186                         } catch (std::bad_cast const&) {
187                             excptr_=std::current_exception();
188                         }
189                         return std::move(ctx);
190                      })
191             {}
192
193             std::string operator()(int i){
194                 boost::variant<int,std::string> data=i;
195                 auto result=ctx_(data);
196                 ctx_=std::move(std::get<0>(result));
197                 data=std::get<1>(result);
198                 if(excptr_){
199                     std::rethrow_exception(excptr_);
200                 }
201                 return boost::get<std::string>(data);
202             }
203         };
204
205         X x;
206         std::cout << x(7) << std::endl;
207
208         output:
209         7
210
211 In the case of unidirectional transfer of data, ['boost::optional<>] or a
212 pointer are appropriate.
213
214
215 [heading exception handling]
216 If the function executed inside a __econtext__ emits ans exception, the
217 application is terminated by calling ['std::terminate()]. ['std::exception_ptr]
218 can be used to transfer exceptions between different execution contexts.
219
220 [important Do not jump from inside a catch block and then re-throw the exception
221 in another execution context.]
222
223 [#ecv2_ontop]
224 [heading Executing function on top of a context]
225 Sometimes it is useful to execute a new function on top of a resumed context.
226 For this purpose __ec_op__ with first argument `exec_ontop_arg` has to be used.
227 The function passed as argument must return a tuple of execution_context and
228 arguments.
229
230     boost::context::execution_context<int> f1(boost::context::execution_context<int> && ctx,int data) {
231         std::cout << "f1: entered first time: " << data << std::endl;
232         std::tie(ctx,data)=ctx(data+1);
233         std::cout << "f1: entered second time: " << data << std::endl;
234         std::tie(ctx,data)=ctx(data+1);
235         std::cout << "f1: entered third time: " << data << std::endl;
236         return std::move(ctx);
237     }
238
239     int f2(int data) {
240         std::cout << "f2: entered: " << data << std::endl;
241         return -1;
242     }
243
244     int data=0;
245     boost::context::execution_context< int > ctx(f1);
246     std::tie(ctx,data)=ctx(data+1);
247     std::cout << "f1: returned first time: " << data << std::endl;
248     std::tie(ctx,data)=ctx(data+1);
249     std::cout << "f1: returned second time: " << data << std::endl;
250     std::tie(ctx,data)=ctx(ctx::exec_ontop_arg,f2,data+1);
251
252     output:
253         f1: entered first time: 1
254         f1: returned first time: 2
255         f1: entered second time: 3
256         f1: returned second time: 4
257         f2: entered: 5
258         f1: entered third time: -1
259
260 The expression `ctx(ctx::exec_ontop_arg,f2,data+1)` executes `f2()` on top of
261 context `ctx`, e.g. an additional stack frame is allocated on top of the context
262 stack (in front of `f1()`). `f2()` returns argument `-1` that will returned by
263 the second invocation of `ctx(data+1)` in `f1()`.
264 [/
265 Another option is to execute a function on top of the context that throws an
266 exception. The thrown exception is catched and re-thrown as nested exception of
267 __ot_error__ from __ec_op__. __ot_error__ gives access to the context that has
268 resumed the current context.
269
270     struct my_exception : public std::runtime_error {
271         my_exception( std::string const& what) :
272             std::runtime_error{ what } {
273         }
274     };
275
276     boost::context::execution_context<void> ctx([](boost::context::execution_context<void> && ctx) {
277         for (;;) {
278             try {
279                     std::cout << "entered" << std::endl;
280                     ctx = ctx();
281                 }
282             } catch ( boost::context::ontop_error const& e) {
283                 try {
284                     std::rethrow_if_nested( e);
285                 } catch ( my_exception const& ex) {
286                     std::cerr << "my_exception: " << ex.what() << std::endl;
287                 }
288                 return e.get_context< void >();
289             }
290         }
291         return std::move( ctx);
292     });
293     ctx = ctx();
294     ctx = ctx();
295     ctx = ctx( boost::context::exec_ontop_arg,[](){ throw my_exception{ "abc" }; });
296
297     output:
298         entered
299         entered
300         my_exception: abc
301
302 In this exception `my_exception` is throw from a function invoked ontop of
303 context `ctx` and catched inside the `for`-loop.
304
305 [heading stack unwinding]
306 On construction of __econtext__ a stack is allocated.
307 If the __context_fn__ returns the stack will be destructed.
308 If the __context_fn__ has not yet returned and the destructor of an valid
309 __econtext__ instance (e.g. ['execution_context::operator bool()] returns
310 `true`) is called, the stack will be destructed too.
311
312 [important Code executed by __context_fn__ must not prevent the propagation of the
313 __forced_unwind__ exception.  Absorbing that exception will cause stack
314 unwinding to fail.  Thus, any code that catches all exceptions must re-throw any
315 pending __forced_unwind__ exception.]
316
317
318 [#ecv2_prealloc]
319 [heading allocating control structures on top of stack]
320 Allocating control structures on top of the stack requires to allocated the
321 __stack_context__ and create the control structure with placement new before
322 __econtext__ is created.
323 [note The user is responsible for destructing the control structure at the top
324 of the stack.]
325
326         // stack-allocator used for (de-)allocating stack
327         fixedsize_stack salloc(4048);
328         // allocate stack space
329         stack_context sctx(salloc.allocate());
330         // reserve space for control structure on top of the stack
331         void * sp=static_cast<char*>(sctx.sp)-sizeof(my_control_structure);
332         std::size_t size=sctx.size-sizeof(my_control_structure);
333         // placement new creates control structure on reserved space
334         my_control_structure * cs=new(sp)my_control_structure(sp,size,sctx,salloc);
335         ...
336         // destructing the control structure
337         cs->~my_control_structure();
338         ...
339         struct my_control_structure  {
340             // captured context
341             execution_context cctx;
342
343             template< typename StackAllocator >
344             my_control_structure(void * sp,std::size_t size,stack_context sctx,StackAllocator salloc) :
345                 // create captured context
346                 cctx(std::allocator_arg,preallocated(sp,size,sctx),salloc,entry_func) {
347             }
348             ...
349         };
350
351
352 [heading inverting the control flow]
353
354         /*
355          * grammar:
356          *   P ---> E '\0'
357          *   E ---> T {('+'|'-') T}
358          *   T ---> S {('*'|'/') S}
359          *   S ---> digit | '(' E ')'
360          */
361         class Parser{
362             // implementation omitted; see examples directory
363         };
364
365         std::istringstream is("1+1");
366         bool done=false;
367         std::exception_ptr except;
368
369         // execute parser in new execution context
370         boost::context::execution_context<char> source(
371                 [&is,&done,&except](ctx::execution_context<char> && sink,char){
372                 // create parser with callback function
373                 Parser p( is,
374                           [&sink](char ch){
375                                 // resume main execution context
376                                 auto result=sink(ch);
377                                 sink=std::move(std::get<0>(result));
378                         });
379                     try {
380                         // start recursive parsing
381                         p.run();
382                     } catch (...) {
383                         // store other exceptions in exception-pointer
384                         except=std::current_exception();
385                     }
386                     // set termination flag
387                     done=true;
388                     // resume main execution context
389                     return std::move(sink);
390                 });
391
392         // user-code pulls parsed data from parser
393         // invert control flow
394         auto result=source('\0');
395         source=std::move(std::get<0>(result));
396         char c=std::get<1>(result);
397         if (except) {
398             std::rethrow_exception(except);
399         }
400         while(!done) {
401             printf("Parsed: %c\n",c);
402             std::tie(source,c)=source('\0');
403             if (except) {
404                 std::rethrow_exception(except);
405             }
406         }
407
408         output:
409             Parsed: 1
410             Parsed: +
411             Parsed: 1
412
413 In this example a recursive descent parser uses a callback to emit a newly
414 passed symbol. Using __econtext__ the control flow can be inverted, e.g. the
415 user-code pulls parsed symbols from the parser - instead to get pushed from the
416 parser (via callback).
417
418 The data (character) is transferred between the two __econtext__.
419
420 If the code executed by __econtext__ emits an exception, the application is
421 terminated. ['std::exception_ptr] can be used to transfer exceptions between
422 different execution contexts.
423
424 Sometimes it is necessary to unwind the stack of an unfinished context to
425 destroy local stack variables so they can release allocated resources (RAII
426 pattern). The user is responsible for this task.
427
428
429 [heading Class `execution_context`]
430
431     struct exec_ontop_arg_t {};
432     const exec_ontop_arg_t exec_ontop_arg{};
433
434     class ontop_error {
435     public:
436         template< typename ... Args >
437         execution_context< Args ... > get_context() const noexcept;
438     }
439
440     template< typename ... Args >
441     class execution_context {
442     public:
443         template< typename Fn, typename ... Params >
444         execution_context( Fn && fn, Params && ... params);
445     
446         template< typename StackAlloc, typename Fn, typename ... Params >
447         execution_context( std::allocator_arg_t, StackAlloc salloc, Fn && fn, Params && ... params);
448     
449         template< typename StackAlloc, typename Fn, typename ... Params >
450         execution_context( std::allocator_arg_t, preallocated palloc, StackAlloc salloc, Fn && fn, Params && ... params);
451     
452         template< typename Fn, typename ... Params >
453         execution_context( std::allocator_arg_t, segemented_stack, Fn && fn, Params && ... params) = delete;
454     
455         template< typename Fn, typename ... Params >
456         execution_context( std::allocator_arg_t, preallocated palloc, segmented, Fn && fn, Params && ... params)= delete;
457
458         ~execution_context();
459     
460         execution_context( execution_context && other) noexcept;
461         execution_context & operator=( execution_context && other) noexcept;
462     
463         execution_context( execution_context const& other) noexcept = delete;
464         execution_context & operator=( execution_context const& other) noexcept = delete;
465     
466         explicit operator bool() const noexcept;
467         bool operator!() const noexcept;
468     
469         std::tuple< execution_context, Args ... > operator()( Args ... args);
470     
471         template< typename Fn >
472         std::tuple< execution_context, Args ... > operator()( exec_ontop_arg_t, Fn && fn, Args ... args);
473     
474         bool operator==( execution_context const& other) const noexcept;
475     
476         bool operator!=( execution_context const& other) const noexcept;
477     
478         bool operator<( execution_context const& other) const noexcept;
479     
480         bool operator>( execution_context const& other) const noexcept;
481     
482         bool operator<=( execution_context const& other) const noexcept;
483     
484         bool operator>=( execution_context const& other) const noexcept;
485     
486         template< typename charT, class traitsT >
487         friend std::basic_ostream< charT, traitsT > &
488         operator<<( std::basic_ostream< charT, traitsT > & os, execution_context const& other);
489     };
490
491 [constructor_heading ecv2..constructor]
492
493     template< typename Fn, typename ... Params >
494     execution_context( Fn && fn, Params && ... params);
495     
496     template< typename StackAlloc, typename Fn, typename ... Params >
497     execution_context( std::allocator_arg_t, StackAlloc salloc, Fn && fn, Params && ... params);
498     
499     template< typename StackAlloc, typename Fn, typename ... Params >
500     execution_context( std::allocator_arg_t, preallocated palloc, StackAlloc salloc, Fn && fn, Params && ... params);
501
502 [variablelist
503 [[Effects:] [Creates a new execution context and prepares the context to execute
504 `fn`. `fixedsize_stack` is used as default stack allocator
505 (stack size == fixedsize_stack::traits::default_size()).
506 The constructor with argument type `preallocated`, is used to create a user
507 defined data [link ecv2_prealloc (for instance additional control structures)] on
508 top of the stack.]]
509 ]
510 [destructor_heading ecv2..destructor destructor]
511
512     ~execution_context();
513
514 [variablelist
515 [[Effects:] [Destructs the associated stack if `*this` is a valid context,
516 e.g. ['execution_context::operator bool()] returns `true`.]]
517 [[Throws:] [Nothing.]]
518 ]
519
520 [move_constructor_heading ecv2..move constructor]
521
522     execution_context( execution_context && other) noexcept;
523
524 [variablelist
525 [[Effects:] [Moves underlying capture record to `*this`.]]
526 [[Throws:] [Nothing.]]
527 ]
528
529 [move_assignment_heading ecv2..move assignment]
530
531     execution_context & operator=( execution_context && other) noexcept;
532
533 [variablelist
534 [[Effects:] [Moves the state of `other` to `*this` using move semantics.]]
535 [[Throws:] [Nothing.]]
536 ]
537
538 [operator_heading ecv2..operator_bool..operator bool]
539
540     explicit operator bool() const noexcept;
541
542 [variablelist
543 [[Returns:] [`true` if `*this` points to a capture record.]]
544 [[Throws:] [Nothing.]]
545 ]
546
547 [operator_heading ecv2..operator_not..operator!]
548
549     bool operator!() const noexcept;
550
551 [variablelist
552 [[Returns:] [`true` if `*this` does not point to a capture record.]]
553 [[Throws:] [Nothing.]]
554 ]
555
556 [operator_heading ecv2..operator_call..operator()]
557
558     std::tuple< execution_context< Args ... >, Args ... > operator()( Args ... args); // member of generic execution_context template
559
560     execution_context< void > operator()(); // member of execution_context< void >
561
562 [variablelist
563 [[Effects:] [Stores internally the current context data (stack pointer,
564 instruction pointer, and CPU registers) of the current active context and
565 restores the context data from `*this`, which implies jumping to `*this`'s
566 context.
567 The arguments, `... args`, are passed to the current context to be returned
568 by the most recent call to `execution_context::operator()` in the same thread.]]
569 [[Returns:] [The tuple of execution_context and returned arguments passed to the
570 most recent call to `execution_context::operator()`, if any and a
571 execution_context representing the context that has been suspended.]]
572 [[Note:] [The returned execution_context indicates if the suspended context has
573 terminated (return from context-function) via `bool operator()`. If the returned
574 execution_context has terminated no data are transferred in the returned tuple.]]
575 ]
576
577 [operator_heading ecv2..operator_call_ontop..operator()]
578
579             template< typename Fn >
580             std::tuple< execution_context< Args ... >, Args ... > operator()( exec_ontop_arg_t, Fn && fn, Args ... args); // member of generic execution_context
581
582             template< typename Fn >
583             execution_context< void > operator()( exec_ontop_arg_t, Fn && fn); // member of execution_context< void >
584
585 [variablelist
586 [[Effects:] [Same as __ec_op__. Additionally, function `fn` is executed
587 in the context of `*this` (e.g. the stack frame of `fn` is allocated on
588 stack of `*this`).]]
589 [[Returns:] [The tuple of execution_context and returned arguments passed to the
590 most recent call to `execution_context::operator()`, if any and a
591 execution_context representing the context that has been suspended .]]
592 [[Note:] [The tuple of execution_context and returned arguments from `fn` are
593 passed as arguments to the context-function of resumed context (if the context
594 is entered the first time) or those arguments are returned from
595 `execution_context::operator()` within the resumed context.]]
596 [[Note:] [Function `fn` needs to return a tuple of arguments
597 ([link ecv2_ontop see description]).]]
598 [[Note:] [The context calling this function must not be destroyed before the
599 arguments, that will be returned from `fn`, are preserved at least in the stack
600 frame of the resumed context.]]
601 [[Note:] [The returned execution_context indicates if the suspended context has
602 terminated (return from context-function) via `bool operator()`. If the returned
603 execution_context has terminated no data are transferred in the returned tuple.]]
604 ]
605
606 [operator_heading ecv2..operator_equal..operator==]
607
608         bool operator==( execution_context const& other) const noexcept;
609
610 [variablelist
611 [[Returns:] [`true` if `*this` and `other` represent the same execution context,
612 `false` otherwise.]]
613 [[Throws:] [Nothing.]]
614 ]
615
616 [operator_heading ecv2..operator_notequal..operator!=]
617
618         bool operator!=( execution_context const& other) const noexcept;
619
620 [variablelist
621 [[Returns:] [[`! (other == * this)]]]
622 [[Throws:] [Nothing.]]
623 ]
624
625 [operator_heading ecv2..operator_less..operator<]
626
627         bool operator<( execution_context const& other) const noexcept;
628
629 [variablelist
630 [[Returns:] [`true` if `*this != other` is true and the
631 implementation-defined total order of `execution_context` values places `*this`
632 before `other`, false otherwise.]]
633 [[Throws:] [Nothing.]]
634 ]
635
636 [operator_heading ecv2..operator_greater..operator>]
637
638         bool operator>( execution_context const& other) const noexcept;
639
640 [variablelist
641 [[Returns:] [`other < * this`]]
642 [[Throws:] [Nothing.]]
643 ]
644
645 [operator_heading ecv2..operator_lesseq..operator<=]
646
647         bool operator<=( execution_context const& other) const noexcept;
648
649 [variablelist
650 [[Returns:] [`! (other < * this)`]]
651 [[Throws:] [Nothing.]]
652 ]
653
654 [operator_heading ecv2..operator_greatereq..operator>=]
655
656         bool operator>=( execution_context const& other) const noexcept;
657
658 [variablelist
659 [[Returns:] [`! (* this < other)`]]
660 [[Throws:] [Nothing.]]
661 ]
662
663 [hding ecv2_..Non-member function [`operator<<()]]
664
665         template< typename charT, class traitsT >
666         std::basic_ostream< charT, traitsT > &
667         operator<<( std::basic_ostream< charT, traitsT > & os, execution_context const& other);
668
669 [variablelist
670 [[Efects:] [Writes the representation of `other` to stream `os`.]]
671 [[Returns:] [`os`]]
672 ]
673
674
675 [endsect]