1 // Written in the D programming language.
2
3 /**
4 * Signals and Slots are an implementation of the Observer Pattern.
5 * Essentially, when a Signal is emitted, a list of connected Observers
6 * (called slots) are called.
7 *
8 * There have been several D implementations of Signals and Slots.
9 * This version makes use of several new features in D, which make
10 * using it simpler and less error prone. In particular, it is no
11 * longer necessary to instrument the slots.
12 *
13 * References:
14 * $(LUCKY A Deeper Look at Signals and Slots)$(BR)
15 * $(LINK2 http://en.wikipedia.org/wiki/Observer_pattern, Observer pattern)$(BR)
16 * $(LINK2 http://en.wikipedia.org/wiki/Signals_and_slots, Wikipedia)$(BR)
17 * $(LINK2 http://boost.org/doc/html/$(SIGNALS).html, Boost Signals)$(BR)
18 * $(LINK2 http://qt-project.org/doc/qt-5/signalsandslots.html, Qt)$(BR)
19 *
20 * There has been a great deal of discussion in the D newsgroups
21 * over this, and several implementations:
22 *
23 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/signal_slots_library_4825.html, signal slots library)$(BR)
24 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Signals_and_Slots_in_D_42387.html, Signals and Slots in D)$(BR)
25 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dynamic_binding_--_Qt_s_Signals_and_Slots_vs_Objective-C_42260.html, Dynamic binding -- Qt's Signals and Slots vs Objective-C)$(BR)
26 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dissecting_the_SS_42377.html, Dissecting the SS)$(BR)
27 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/dwt/about_harmonia_454.html, about harmonia)$(BR)
28 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/1502.html, Another event handling module)$(BR)
29 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/41825.html, Suggestion: signal/slot mechanism)$(BR)
30 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/13251.html, Signals and slots?)$(BR)
31 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/10714.html, Signals and slots ready for evaluation)$(BR)
32 * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/1393.html, Signals & Slots for Walter)$(BR)
33 * $(LINK2 http://www.digitalmars.com/d/archives/28456.html, Signal/Slot mechanism?)$(BR)
34 * $(LINK2 http://www.digitalmars.com/d/archives/19470.html, Modern Features?)$(BR)
35 * $(LINK2 http://www.digitalmars.com/d/archives/16592.html, Delegates vs interfaces)$(BR)
36 * $(LINK2 http://www.digitalmars.com/d/archives/16583.html, The importance of component programming (properties$(COMMA) signals and slots$(COMMA) etc))$(BR)
37 * $(LINK2 http://www.digitalmars.com/d/archives/16368.html, signals and slots)$(BR)
38 *
39 * Bugs:
40 * $(RED Slots can only be delegates referring directly to
41 * class or interface member functions. If a delegate to something else
42 * is passed to connect(), such as a struct member function,
43 * a nested function, a COM interface, a closure, undefined behavior
44 * will result.)
45 *
46 * Not safe for multiple threads operating on the same signals
47 * or slots.
48 * Macros:
49 * SIGNALS=signals
50 *
51 * Copyright: Copyright The D Language Foundation 2000 - 2009.
52 * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
53 * Authors: $(HTTP digitalmars.com, Walter Bright)
54 * Source: $(PHOBOSSRC std/signals.d)
55 *
56 * $(SCRIPT inhibitQuickIndex = 1;)
57 */
58 /* Copyright The D Language Foundation 2000 - 2009.
59 * Distributed under the Boost Software License, Version 1.0.
60 * (See accompanying file LICENSE_1_0.txt or copy at
61 * http://www.boost.org/LICENSE_1_0.txt)
62 */
63 module std.signals;
64
65 import core.exception : onOutOfMemoryError;
66 import core.stdc.stdlib : calloc, realloc, free;
67 import std.stdio;
68
69 // Special function for internal use only.
70 // Use of this is where the slot had better be a delegate
71 // to an object or an interface that is part of an object.
72 extern (C) Object _d_toObject(void* p);
73
74 // Used in place of Object.notifyRegister and Object.notifyUnRegister.
75 alias DisposeEvt = void delegate(Object);
76 extern (C) void rt_attachDisposeEvent( Object obj, DisposeEvt evt );
77 extern (C) void rt_detachDisposeEvent( Object obj, DisposeEvt evt );
78 //debug=signal;
79
80 /************************
81 * Mixin to create a signal within a class object.
82 *
83 * Different signals can be added to a class by naming the mixins.
84 */
85
Signal(T1...)86 mixin template Signal(T1...)
87 {
88 static import core.exception;
89 static import core.stdc.stdlib;
90 /***
91 * A slot is implemented as a delegate.
92 * The slot_t is the type of the delegate.
93 * The delegate must be to an instance of a class or an interface
94 * to a class instance.
95 * Delegates to struct instances or nested functions must not be
96 * used as slots. This applies even if the nested function does not access
97 * it's parent function variables.
98 */
99 alias slot_t = void delegate(T1);
100
101 /***
102 * Call each of the connected slots, passing the argument(s) i to them.
103 * Nested call will be ignored.
104 */
105 final void emit( T1 i )
106 {
107 if (status >= ST.inemitting || !slots.length)
108 return; // should not nest
109
110 status = ST.inemitting;
111 scope (exit)
112 status = ST.idle;
113
114 foreach (slot; slots[0 .. slots_idx])
115 { if (slot)
116 slot(i);
117 }
118
119 assert(status >= ST.inemitting);
120 if (status == ST.inemitting_disconnected)
121 {
122 for (size_t j = 0; j < slots_idx;)
123 {
124 if (slots[j] is null)
125 {
126 slots_idx--;
127 slots[j] = slots[slots_idx];
128 }
129 else
130 j++;
131 }
132 }
133 }
134
135 /***
136 * Add a slot to the list of slots to be called when emit() is called.
137 */
138 final void connect(slot_t slot)
139 {
140 /* Do this:
141 * slots ~= slot;
142 * but use malloc() and friends instead
143 */
144 auto len = slots.length;
145 if (slots_idx == len)
146 {
147 if (slots.length == 0)
148 {
149 len = 4;
150 auto p = core.stdc.stdlib.calloc(slot_t.sizeof, len);
151 if (!p)
152 core.exception.onOutOfMemoryError();
153 slots = (cast(slot_t*) p)[0 .. len];
154 }
155 else
156 {
157 import core.checkedint : addu, mulu;
158 bool overflow;
159 len = addu(mulu(len, 2, overflow), 4, overflow); // len = len * 2 + 4
160 const nbytes = mulu(len, slot_t.sizeof, overflow);
161 if (overflow) assert(0);
162
163 auto p = core.stdc.stdlib.realloc(slots.ptr, nbytes);
164 if (!p)
165 core.exception.onOutOfMemoryError();
166 slots = (cast(slot_t*) p)[0 .. len];
167 slots[slots_idx + 1 .. $] = null;
168 }
169 }
170 slots[slots_idx++] = slot;
171
172 L1:
173 Object o = _d_toObject(slot.ptr);
174 rt_attachDisposeEvent(o, &unhook);
175 }
176
177 /***
178 * Remove a slot from the list of slots to be called when emit() is called.
179 */
180 final void disconnect(slot_t slot)
181 {
182 debug (signal) writefln("Signal.disconnect(slot)");
183 size_t disconnectedSlots = 0;
184 size_t instancePreviousSlots = 0;
185 if (status >= ST.inemitting)
186 {
187 foreach (i, sloti; slots[0 .. slots_idx])
188 {
189 if (sloti.ptr == slot.ptr &&
190 ++instancePreviousSlots &&
191 sloti == slot)
192 {
193 disconnectedSlots++;
194 slots[i] = null;
195 status = ST.inemitting_disconnected;
196 }
197 }
198 }
199 else
200 {
201 for (size_t i = 0; i < slots_idx; )
202 {
203 if (slots[i].ptr == slot.ptr &&
204 ++instancePreviousSlots &&
205 slots[i] == slot)
206 {
207 slots_idx--;
208 disconnectedSlots++;
209 slots[i] = slots[slots_idx];
210 slots[slots_idx] = null; // not strictly necessary
211 }
212 else
213 i++;
214 }
215 }
216
217 // detach object from dispose event if all its slots have been removed
218 if (instancePreviousSlots == disconnectedSlots)
219 {
220 Object o = _d_toObject(slot.ptr);
221 rt_detachDisposeEvent(o, &unhook);
222 }
223 }
224
225 /***
226 * Disconnect all the slots.
227 */
228 final void disconnectAll()
229 {
230 debug (signal) writefln("Signal.disconnectAll");
231 __dtor();
232 slots_idx = 0;
233 status = ST.idle;
234 }
235
236 /* **
237 * Special function called when o is destroyed.
238 * It causes any slots dependent on o to be removed from the list
239 * of slots to be called by emit().
240 */
241 final void unhook(Object o)
242 in { assert( status == ST.idle ); }
243 do
244 {
245 debug (signal) writefln("Signal.unhook(o = %s)", cast(void*) o);
246 for (size_t i = 0; i < slots_idx; )
247 {
248 if (_d_toObject(slots[i].ptr) is o)
249 { slots_idx--;
250 slots[i] = slots[slots_idx];
251 slots[slots_idx] = null; // not strictly necessary
252 }
253 else
254 i++;
255 }
256 }
257
258 /* **
259 * There can be multiple destructors inserted by mixins.
260 */
261 ~this()
262 {
263 /* **
264 * When this object is destroyed, need to let every slot
265 * know that this object is destroyed so they are not left
266 * with dangling references to it.
267 */
268 if (slots.length)
269 {
270 foreach (slot; slots[0 .. slots_idx])
271 {
272 if (slot)
273 { Object o = _d_toObject(slot.ptr);
274 rt_detachDisposeEvent(o, &unhook);
275 }
276 }
277 core.stdc.stdlib.free(slots.ptr);
278 slots = null;
279 }
280 }
281
282 private:
283 slot_t[] slots; // the slots to call from emit()
284 size_t slots_idx; // used length of slots[]
285
286 enum ST { idle, inemitting, inemitting_disconnected }
287 ST status;
288 }
289
290 ///
291 @system unittest
292 {
293 import std.signals;
294
295 int observedMessageCounter = 0;
296
297 class Observer
298 { // our slot
watch(string msg,int value)299 void watch(string msg, int value)
300 {
301 switch (observedMessageCounter++)
302 {
303 case 0:
304 assert(msg == "setting new value");
305 assert(value == 4);
306 break;
307 case 1:
308 assert(msg == "setting new value");
309 assert(value == 6);
310 break;
311 default:
312 assert(0, "Unknown observation");
313 }
314 }
315 }
316
317 class Observer2
318 { // our slot
watch(string msg,int value)319 void watch(string msg, int value)
320 {
321 }
322 }
323
324 class Foo
325 {
value()326 int value() { return _value; }
327
value(int v)328 int value(int v)
329 {
330 if (v != _value)
331 { _value = v;
332 // call all the connected slots with the two parameters
333 emit("setting new value", v);
334 }
335 return v;
336 }
337
338 // Mix in all the code we need to make Foo into a signal
339 mixin Signal!(string, int);
340
341 private :
342 int _value;
343 }
344
345 Foo a = new Foo;
346 Observer o = new Observer;
347 auto o2 = new Observer2;
348 auto o3 = new Observer2;
349 auto o4 = new Observer2;
350 auto o5 = new Observer2;
351
352 a.value = 3; // should not call o.watch()
353 a.connect(&o.watch); // o.watch is the slot
354 a.connect(&o2.watch);
355 a.connect(&o3.watch);
356 a.connect(&o4.watch);
357 a.connect(&o5.watch);
358 a.value = 4; // should call o.watch()
359 a.disconnect(&o.watch); // o.watch is no longer a slot
360 a.disconnect(&o3.watch);
361 a.disconnect(&o5.watch);
362 a.disconnect(&o4.watch);
363 a.disconnect(&o2.watch);
364 a.value = 5; // so should not call o.watch()
365 a.connect(&o2.watch);
366 a.connect(&o.watch); // connect again
367 a.value = 6; // should call o.watch()
368 destroy(o); // destroying o should automatically disconnect it
369 a.value = 7; // should not call o.watch()
370
371 assert(observedMessageCounter == 2);
372 }
373
374 // A function whose sole purpose is to get this module linked in
375 // so the unittest will run.
linkin()376 void linkin() { }
377
378 @system unittest
379 {
380 class Observer
381 {
watch(string msg,int i)382 void watch(string msg, int i)
383 {
384 //writefln("Observed msg '%s' and value %s", msg, i);
385 captured_value = i;
386 captured_msg = msg;
387 }
388
389 int captured_value;
390 string captured_msg;
391 }
392
393 class Foo
394 {
value()395 @property int value() { return _value; }
396
value(int v)397 @property int value(int v)
398 {
399 if (v != _value)
400 { _value = v;
401 emit("setting new value", v);
402 }
403 return v;
404 }
405
406 mixin Signal!(string, int);
407
408 private:
409 int _value;
410 }
411
412 Foo a = new Foo;
413 Observer o = new Observer;
414
415 // check initial condition
416 assert(o.captured_value == 0);
417 assert(o.captured_msg == "");
418
419 // set a value while no observation is in place
420 a.value = 3;
421 assert(o.captured_value == 0);
422 assert(o.captured_msg == "");
423
424 // connect the watcher and trigger it
425 a.connect(&o.watch);
426 a.value = 4;
427 assert(o.captured_value == 4);
428 assert(o.captured_msg == "setting new value");
429
430 // disconnect the watcher and make sure it doesn't trigger
431 a.disconnect(&o.watch);
432 a.value = 5;
433 assert(o.captured_value == 4);
434 assert(o.captured_msg == "setting new value");
435
436 // reconnect the watcher and make sure it triggers
437 a.connect(&o.watch);
438 a.value = 6;
439 assert(o.captured_value == 6);
440 assert(o.captured_msg == "setting new value");
441
442 // destroy the underlying object and make sure it doesn't cause
443 // a crash or other problems
444 destroy(o);
445 a.value = 7;
446 }
447
448 @system unittest
449 {
450 class Observer
451 {
452 int i;
453 long l;
454 string str;
455
watchInt(string str,int i)456 void watchInt(string str, int i)
457 {
458 this.str = str;
459 this.i = i;
460 }
461
watchLong(string str,long l)462 void watchLong(string str, long l)
463 {
464 this.str = str;
465 this.l = l;
466 }
467 }
468
469 class Bar
470 {
value1(int v)471 @property void value1(int v) { s1.emit("str1", v); }
value2(int v)472 @property void value2(int v) { s2.emit("str2", v); }
value3(long v)473 @property void value3(long v) { s3.emit("str3", v); }
474
475 mixin Signal!(string, int) s1;
476 mixin Signal!(string, int) s2;
477 mixin Signal!(string, long) s3;
478 }
479
test(T)480 void test(T)(T a) {
481 auto o1 = new Observer;
482 auto o2 = new Observer;
483 auto o3 = new Observer;
484
485 // connect the watcher and trigger it
486 a.s1.connect(&o1.watchInt);
487 a.s2.connect(&o2.watchInt);
488 a.s3.connect(&o3.watchLong);
489
490 assert(!o1.i && !o1.l && o1.str == null);
491 assert(!o2.i && !o2.l && o2.str == null);
492 assert(!o3.i && !o3.l && o3.str == null);
493
494 a.value1 = 11;
495 assert(o1.i == 11 && !o1.l && o1.str == "str1");
496 assert(!o2.i && !o2.l && o2.str == null);
497 assert(!o3.i && !o3.l && o3.str == null);
498 o1.i = -11; o1.str = "x1";
499
500 a.value2 = 12;
501 assert(o1.i == -11 && !o1.l && o1.str == "x1");
502 assert(o2.i == 12 && !o2.l && o2.str == "str2");
503 assert(!o3.i && !o3.l && o3.str == null);
504 o2.i = -12; o2.str = "x2";
505
506 a.value3 = 13;
507 assert(o1.i == -11 && !o1.l && o1.str == "x1");
508 assert(o2.i == -12 && !o1.l && o2.str == "x2");
509 assert(!o3.i && o3.l == 13 && o3.str == "str3");
510 o3.l = -13; o3.str = "x3";
511
512 // disconnect the watchers and make sure it doesn't trigger
513 a.s1.disconnect(&o1.watchInt);
514 a.s2.disconnect(&o2.watchInt);
515 a.s3.disconnect(&o3.watchLong);
516
517 a.value1 = 21;
518 a.value2 = 22;
519 a.value3 = 23;
520 assert(o1.i == -11 && !o1.l && o1.str == "x1");
521 assert(o2.i == -12 && !o1.l && o2.str == "x2");
522 assert(!o3.i && o3.l == -13 && o3.str == "x3");
523
524 // reconnect the watcher and make sure it triggers
525 a.s1.connect(&o1.watchInt);
526 a.s2.connect(&o2.watchInt);
527 a.s3.connect(&o3.watchLong);
528
529 a.value1 = 31;
530 a.value2 = 32;
531 a.value3 = 33;
532 assert(o1.i == 31 && !o1.l && o1.str == "str1");
533 assert(o2.i == 32 && !o1.l && o2.str == "str2");
534 assert(!o3.i && o3.l == 33 && o3.str == "str3");
535
536 // destroy observers
537 destroy(o1);
538 destroy(o2);
539 destroy(o3);
540 a.value1 = 41;
541 a.value2 = 42;
542 a.value3 = 43;
543 }
544
545 test(new Bar);
546
547 class BarDerived: Bar
548 {
value4(int v)549 @property void value4(int v) { s4.emit("str4", v); }
value5(int v)550 @property void value5(int v) { s5.emit("str5", v); }
value6(long v)551 @property void value6(long v) { s6.emit("str6", v); }
552
553 mixin Signal!(string, int) s4;
554 mixin Signal!(string, int) s5;
555 mixin Signal!(string, long) s6;
556 }
557
558 auto a = new BarDerived;
559
560 test!Bar(a);
561 test!BarDerived(a);
562
563 auto o4 = new Observer;
564 auto o5 = new Observer;
565 auto o6 = new Observer;
566
567 // connect the watcher and trigger it
568 a.s4.connect(&o4.watchInt);
569 a.s5.connect(&o5.watchInt);
570 a.s6.connect(&o6.watchLong);
571
572 assert(!o4.i && !o4.l && o4.str == null);
573 assert(!o5.i && !o5.l && o5.str == null);
574 assert(!o6.i && !o6.l && o6.str == null);
575
576 a.value4 = 44;
577 assert(o4.i == 44 && !o4.l && o4.str == "str4");
578 assert(!o5.i && !o5.l && o5.str == null);
579 assert(!o6.i && !o6.l && o6.str == null);
580 o4.i = -44; o4.str = "x4";
581
582 a.value5 = 45;
583 assert(o4.i == -44 && !o4.l && o4.str == "x4");
584 assert(o5.i == 45 && !o5.l && o5.str == "str5");
585 assert(!o6.i && !o6.l && o6.str == null);
586 o5.i = -45; o5.str = "x5";
587
588 a.value6 = 46;
589 assert(o4.i == -44 && !o4.l && o4.str == "x4");
590 assert(o5.i == -45 && !o4.l && o5.str == "x5");
591 assert(!o6.i && o6.l == 46 && o6.str == "str6");
592 o6.l = -46; o6.str = "x6";
593
594 // disconnect the watchers and make sure it doesn't trigger
595 a.s4.disconnect(&o4.watchInt);
596 a.s5.disconnect(&o5.watchInt);
597 a.s6.disconnect(&o6.watchLong);
598
599 a.value4 = 54;
600 a.value5 = 55;
601 a.value6 = 56;
602 assert(o4.i == -44 && !o4.l && o4.str == "x4");
603 assert(o5.i == -45 && !o4.l && o5.str == "x5");
604 assert(!o6.i && o6.l == -46 && o6.str == "x6");
605
606 // reconnect the watcher and make sure it triggers
607 a.s4.connect(&o4.watchInt);
608 a.s5.connect(&o5.watchInt);
609 a.s6.connect(&o6.watchLong);
610
611 a.value4 = 64;
612 a.value5 = 65;
613 a.value6 = 66;
614 assert(o4.i == 64 && !o4.l && o4.str == "str4");
615 assert(o5.i == 65 && !o4.l && o5.str == "str5");
616 assert(!o6.i && o6.l == 66 && o6.str == "str6");
617
618 // destroy observers
619 destroy(o4);
620 destroy(o5);
621 destroy(o6);
622 a.value4 = 44;
623 a.value5 = 45;
624 a.value6 = 46;
625 }
626
627 // Triggers bug from issue 15341
628 @system unittest
629 {
630 class Observer
631 {
watch()632 void watch() { }
watch2()633 void watch2() { }
634 }
635
636 class Bar
637 {
638 mixin Signal!();
639 }
640
641 auto a = new Bar;
642 auto o = new Observer;
643
644 //Connect both observer methods for the same instance
645 a.connect(&o.watch);
646 a.connect(&o.watch2); // not connecting watch2() or disconnecting it manually fixes the issue
647
648 //Disconnect a single method of the two
649 a.disconnect(&o.watch); // NOT disconnecting watch() fixes the issue
650
651 destroy(o); // destroying o should automatically call unhook and disconnect the slot for watch2
652 a.emit(); // should not raise segfault since &o.watch2 is no longer connected
653 }
654
version(none)655 version (none) // Disabled because of https://issues.dlang.org/show_bug.cgi?id=5028
656 @system unittest
657 {
658 class A
659 {
660 mixin Signal!(string, int) s1;
661 }
662
663 class B : A
664 {
665 mixin Signal!(string, int) s2;
666 }
667 }
668
669 // Triggers bug from issue 16249
670 @system unittest
671 {
672 class myLINE
673 {
674 mixin Signal!( myLINE, int );
675
value(int v)676 void value( int v )
677 {
678 if ( v >= 0 ) emit( this, v );
679 else emit( new myLINE, v );
680 }
681 }
682
683 class Dot
684 {
685 int value;
686
687 myLINE line_;
line(myLINE line_x)688 void line( myLINE line_x )
689 {
690 if ( line_ is line_x ) return;
691
692 if ( line_ !is null )
693 {
694 line_.disconnect( &watch );
695 }
696 line_ = line_x;
697 line_.connect( &watch );
698 }
699
watch(myLINE line_x,int value_x)700 void watch( myLINE line_x, int value_x )
701 {
702 line = line_x;
703 value = value_x;
704 }
705 }
706
707 auto dot1 = new Dot;
708 auto dot2 = new Dot;
709 auto line = new myLINE;
710 dot1.line = line;
711 dot2.line = line;
712
713 line.value = 11;
714 assert( dot1.value == 11 );
715 assert( dot2.value == 11 );
716
717 line.value = -22;
718 assert( dot1.value == -22 );
719 assert( dot2.value == -22 );
720 }
721
722 @system unittest
723 {
724 import std.signals;
725
726 class Observer
727 { // our slot
watch(string msg,int value)728 void watch(string msg, int value)
729 {
730 if (value != 0)
731 {
732 assert(msg == "setting new value");
733 assert(value == 1);
734 }
735 }
736 }
737
738 class Foo
739 {
value()740 int value() { return _value; }
741
value(int v)742 int value(int v)
743 {
744 if (v != _value)
745 {
746 _value = v;
747 // call all the connected slots with the parameters
748 emit("setting new value", v);
749 }
750 return v;
751 }
752
753 // Mix in all the code we need to make Foo into a signal
754 mixin Signal!(string, int);
755
756 private :
757 int _value;
758 }
759
760 Foo a = new Foo;
761 Observer o = new Observer;
762 auto o2 = new Observer;
763
764 a.value = 3; // should not call o.watch()
765 a.connect(&o.watch); // o.watch is the slot
766 a.connect(&o2.watch);
767 a.value = 1; // should call o.watch()
768 a.disconnectAll();
769 a.value = 5; // so should not call o.watch()
770 a.connect(&o.watch); // connect again
771 a.connect(&o2.watch);
772 a.value = 1; // should call o.watch()
773 destroy(o); // destroying o should automatically disconnect it
774 destroy(o2);
775 a.value = 7; // should not call o.watch()
776 }
777