Path: news.daimi.aau.dk!news.daimi.aau.dk!eernst From: eernst@quercus.daimi.aau.dk (Erik Ernst) Newsgroups: comp.lang.beta Subject: Just add water (from the OUTER space)! Followup-To: comp.lang.beta Date: 15 Jun 1995 00:22:51 GMT Organization: DAIMI, Computer Science Dept. of Aarhus Univ. Lines: 855 Distribution: world Message-ID: NNTP-Posting-Host: quercus.daimi.aau.dk I know this is a long news posting. It contains a number of small but complete BETA programs and a small supporting library, and it is my intention to make it possible for you to experiment with these things yourself. Therefore I couldn't easily make it smaller. Forgive me for the possible misuse of network bandwidth. A couple of weeks ago there was a discussion on c.l.beta about `OUTER' and `weak inheritance' and similar (not-yet-existing) things. There were a couple of interesting proposals to generalize the mechanism that I described (in ): On 17 May 1995 10:18:18 GMT, Marek Novy wrote: > > Also speaking to Erik. What do you say to that: > > :! > (# ..... do ;#); > > where: Object >= 'functional-ancestor' >= 'structural-ancestor' > So it means the 'over' will be bound to the 'functional-ancestor' inner. > Outer has similar meaning, it directly calls imperatives replaced by > over. Also inner is similar, it calls indirectly imperatives of a > functional descendant. > > Limit cases: > > B:A!A(#....#); is equal to usual B:A(#....#); > C:A!Object(#....#); is equal to Erik's C:!A(#....#); > > Implementation is straightforward using inner dispatch table. > Is it right? > - and On 24 May 95 15:26:32 GMT, wpp@lise.physik.tu-berlin.de (Kai Petzke) wrote: > > As far, as I understand the override operator, it will override > all the do parts on top of the pattern, that is defined with that > operator. Would it also make sense to have an override operator, > that overrides only a given number of do-Parts, or does that get > too complex? > I've been thinking about these generalizations (which I interpret as essentially the same thing), and also experimenting with several possible semantics. What follows is a description of a concrete implementation (that you can experiment with if you have a BETA compiler) which at the same time serves as a precise semantic specification of the new language mechanisms. I'll be using Marek's syntax, i.e. c: b!a(# ... #); means that `c' is a direct specialization of `b' wrt attributes, wrt enter- and exit-parts, and wrt type-checking; and that it is a direct specialization of `a' wrt the do-part. My conclusions: =============== * There are obvious syntactical problems. One wouldn't want to have to refer to patterns by names, because many patterns (esp. virtuals) are anonymous at the source code level. One would only have to specify the _number of levels to skip_ in the generalization path (from "me" towards `object'), as this uniquely determines the direct functional ancestor. But using numbers for this (like `c: b!2(# ... #)') seems unusual and kludgy. Also, `c: b!!(# ... #)' gets weird for the (probably typical) case of skipping all levels up to `object', which could end up like `c: b!!!!!!!!(# ... #)'. Perhaps: numbers for the normal case ("skip 2 levels" is like `c: b!2(# ... #)'), and "skip all levels up to object" is like `c: b!(# .. #)'. This is the notation used in this posting. Of course, virtual declarations of all kinds would stay the way they are today, only the prefix field of an object descriptor may change: a: (# v:< w #); b: a(# v::< !(# do #) #); c: b(# v:: !2(# do #) #); * This general mechanism makes it possible to taylor a wide range of method combination patterns [sic!] such as: normal BETA method combination, nesting inwards; smalltalk style outward nesting, obtained by inheriting like Pn+1: Pn!(# do ... ... #); then there's the possibility to override; and one can design and use numerous other possible patterns of usage. The usage would have to be disciplined---following certain application- and location-specific rules---to avoid amok-running, incomprehensible complexity to arise in pratical coding. Then again, `leave', `restart', inheritance, fire, women, _everything_ is potentially dangerous. * The full generality of the proposal implies a more costly execution model. It will not be possible to look up in a per-pattern compile-time decidable table associated the current object, in order to decide where to jump when executing an `INNER' imperative. (More on this along with one of the examples). * With the lesser generality of my original proposal (which is like the general model except that non-standard inheritance must skip all levels up to `object' (i.e. things like `c: b!2(# ... #)' are banned, but `c: b!(# ... #)' is OK), it is possible to compile to code which is as efficient as today (one extra, statically known value must be delivered in a register for virtuals, that's all). Just add water: =============== Now to the good stuff: A number of small examples in the extended and standard language, along with comments on what they are good for. Firstly, a description of how to transform code from extended BETA to standard BETA. This transformation is described stepwise, where '-->' denotes one step in the process. To support weak inheritance, an object must eventually derive from `_object', a: (# do 'a'->o; inner; 'a'->o #); b: a(# do 'b'->o; inner; 'b'->o #); c: b!2(# do 'c'->o; outer; 'c'->o #); d: c(# do 'X'->o #); --> a: _object(# do 'a'->o; inner; 'a'->o #); b: a(# do 'b'->o; inner; 'b'->o #); c: b!2(# do 'c'->o; outer; 'c'->o #); d: c(# do 'X'->o #); Put the do-part into a new nested pattern, specialized from `do_ptn': --> a: _object(# a_do: do_ptn(# do 'a'->o; inner; 'a'->o #)#); b: a(# b_do: do_ptn(# do 'b'->o; inner; 'b'->o #)#); c: b!a(# c_do: do_ptn(# do 'c'->o; outer; 'c'->o #)#); d: c(# d_do: do_ptn(# do 'X'->o #)#); Change `INNER' to `do_inner' and `OUTER' to `do_outer': --> a: _object(# a_do: do_ptn(# do 'a'->o; do_inner; 'a'->o #)#); b: a(# b_do: do_ptn(# do 'b'->o; do_inner; 'b'->o #)#); c: b!a(# c_do: do_ptn(# do 'c'->o; do_outer; 'c'->o #)#); d: c(# d_do: do_ptn(# do 'X'->o #)#); Register the "do-part" as the do-part of this pattern by means of a specialization of `init': --> a: _object(# init::< (# do (a_do##,2,1)->insert_dopart; INNER #); a_do: do_ptn(# do 'a'->o; do_inner; 'a'->o #)#); b: a (# init::< (# do (b_do##,3,1)->insert_dopart; INNER #); b_do: do_ptn(# do 'b'->o; do_inner; 'b'->o #)#); c: b(*!2*)(# init::< (# do (c_do##,4,2)->insert_dopart; INNER #); c_do: do_ptn(# do 'c'->o; do_outer; 'c'->o #)#); d: c (# init::< (# do (d_do##,5,1)->insert_dopart; INNER #); d_do: do_ptn(# do 'X'->o #)#); `insert_dopart' takes as argument the pattern in question, the static level of specialization (where `_object' always is at level 1), and the number of levels to skip in the generalization chain to get to the direct functional ancestor. This means e.g. that normal inheritance always has 1 as the last argument to `insert_dopart', and that skipping all levels up to `_object' is specified with an argument list like "(some_do##,n,n-1)". That's it! The shape of the specialization hierarchy is unchanged (apart from having `_object' at the top), and enter- and exit-parts and all kinds of attributes are unaffected. Unless of course you want to transform some of the attributes, too. The Examples: ============= The examples are similar in structure. At the top is the extended BETA source enclosed in a comment, in the middle is the same code transformed into standard BETA, and at the bottom objects are created and executed. After each creation/execution is a comment showing the output it produces. Standard inheritance is straightforward and only serves to show that the semantics are unaffected (backward compatibility): ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE 'weakInherit'; -- program:descriptor -- (# o: (# t: ^text enter t[] do t[]->puttext #); (* Simulating the following patterns: * * a: (# do 'a'->o; inner; 'a'->o #); * b: a(# do 'b'->o; inner; 'b'->o #); * c: b(# do 'c'->o; inner; 'c'->o #); * d: c(# do 'X'->o #); *) a: _object(# init::< (# do (a_do##,2,1)->insert_dopart; INNER #); a_do: do_ptn(# do 'a'->o; do_inner; 'a'->o #)#); b: a (# init::< (# do (b_do##,3,1)->insert_dopart; INNER #); b_do: do_ptn(# do 'b'->o; do_inner; 'b'->o #)#); c: b (# init::< (# do (c_do##,4,1)->insert_dopart; INNER #); c_do: do_ptn(# do 'c'->o; do_inner; 'c'->o #)#); d: c (# init::< (# do (d_do##,5,1)->insert_dopart; INNER #); d_do: do_ptn(# do 'X'->o #)#); do &a; newline; (* "aa" *) &b; newline; (* "abba" *) &c; newline; (* "abccba" *) &d; newline; (* "abcXcba" *) #) ---------------------------------------------------------------------- The method combination is like: *** *** *** *** *** *** *** *** *** *** *** *** *** *** An example of Smalltalk style outward nesting follows. For the patterns `a'..'d', you can ignore the `INNER' imperatives. They all amount to nothing, because the semantics of `INNER' propagates unchanged through levels of (more or less) weak inheritance, and there is weak inheritance all the way from the most specific pattern for both `a', `b', `c' and `d'. For the immediate specialization of `d' at the bottom of the code, however, `INNER' amounts to printing an underscore. As always, the meaning of `INNER' propagates, and the unserscore ends up being printed in three places. The method combination for `a'..`d' is like: *** *** *** *** *** *** *** *** *** *** *** *** *** *** Here's the code: ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE 'weakInherit'; -- program:descriptor -- (# o: (# t: ^text enter t[] do t[]->puttext #); (* Simulating the following patterns: * * a: (# do 'X'->o #); * b: a!(# do 'b'->o; outer; inner; 'b'->o #); * c: b!(# do 'c'->o; outer; inner; 'c'->o #); * d: c!(# do 'd'->o; outer; inner; 'd'->o #); * d(# do '_'->o #); *) a: _object(# init::< (# do (a_do##,2,1)->insert_dopart; INNER #); a_do: do_ptn(# do 'X'->o #)#); b: a(*!*) (# init::< (# do (b_do##,3,2)->insert_dopart; INNER #); b_do: do_ptn(# do 'b'->o; do_outer; do_inner; 'b'->o #)#); c: b(*!*) (# init::< (# do (c_do##,4,3)->insert_dopart; INNER #); c_do: do_ptn(# do 'c'->o; do_outer; do_inner; 'c'->o #)#); d: c(*!*) (# init::< (# do (d_do##,5,4)->insert_dopart; INNER #); d_do: do_ptn(# do 'd'->o; do_outer; do_inner; 'd'->o #)#); do &a; newline; (* "X" *) &b; newline; (* "bXb" *) &c; newline; (* "cbXbc" *) &d; newline; (* "dcbXbcd" *) d(# init:: (# do (_do##,6,1)->insert_dopart; INNER #); _do: do_ptn(# do '_'->o #); #); newline; (* "dcbX_b_c_d" *) #) ---------------------------------------------------------------------- Next comes an example of "semi-weak" inheritance, i.e. skipping some but not all levels upwards. This leads to a method combination which can be illustrated with: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Cute, aint it? ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE 'weakInherit'; -- program:descriptor -- (# o: (# t: ^text enter t[] do t[]->puttext #); (* Simulating the following patterns: * * a: (# do 'a'->o; inner; 'a'->o #); * b: a(# do 'b'->o; inner; 'b'->o #); * c: b!2(# do 'c'->o; outer; 'c'->o #); * d: c(# do 'X'->o #); *) a: _object(# init::< (# do (a_do##,2,1)->insert_dopart; INNER #); a_do: do_ptn(# do 'a'->o; do_inner; 'a'->o #)#); b: a (# init::< (# do (b_do##,3,1)->insert_dopart; INNER #); b_do: do_ptn(# do 'b'->o; do_inner; 'b'->o #)#); c: b(*!2*)(# init::< (# do (c_do##,4,2)->insert_dopart; INNER #); c_do: do_ptn(# do 'c'->o; do_outer; 'c'->o #)#); d: c (# init::< (# do (d_do##,5,1)->insert_dopart; INNER #); d_do: do_ptn(# do 'X'->o #)#); do &a; newline; (* "aa" *) &b; newline; (* "abba" *) &c; newline; (* "acabbaca" *) &d; newline; (* "acabXbaca" *) #) ---------------------------------------------------------------------- One could codify standardize ways of using inheritance in order to support specific implementation tasks in a flexible but regular and understandable manner. Here's an example of a pattern of usage which produces an new and quite unusual regularity: *** *** *** *** *** *** *** *** *** *** *** .. *** .. Who knows what could be invented? (Or what this one would be good for! ;-) By the way, this example only uses my original, restricted version of weak inheritance. ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE 'weakInherit'; -- program:descriptor -- (# o: (# t: ^text enter t[] do t[]->puttext #); (* Simulating the following patterns: * * a: (# do '1'->o; inner #); * b: a!(# do outer; 'a'->o #); * c: b(# do '2'->o; inner #); * d: c!(# do outer; 'b'->o #); * e: d(# do '3'->o; inner #); * f: e!(# do outer; 'c'->o #); *) a: _object(# init::< (# do (a_do##,2,1)->insert_dopart; INNER #); a_do: do_ptn(# do '1'->o; do_inner #)#); b: a(*!*) (# init::< (# do (b_do##,3,2)->insert_dopart; INNER #); b_do: do_ptn(# do do_outer; 'a'->o #)#); c: b (# init::< (# do (c_do##,4,1)->insert_dopart; INNER #); c_do: do_ptn(# do '2'->o; do_inner #)#); d: c(*!*) (# init::< (# do (d_do##,5,4)->insert_dopart; INNER #); d_do: do_ptn(# do do_outer; 'b'->o #)#); e: d (# init::< (# do (e_do##,6,1)->insert_dopart; INNER #); e_do: do_ptn(# do '3'->o; do_inner #)#); f: e(*!*) (# init::< (# do (f_do##,7,6)->insert_dopart; INNER #); f_do: do_ptn(# do do_outer; 'c'->o #)#); do &a; newline; (* "1" *) &b; newline; (* "1a" *) &c; newline; (* "12a" *) &d; newline; (* "12ab" *) &e; newline; (* "123ab" *) &f; newline; (* "123abc" *) #) ---------------------------------------------------------------------- The next example is crucial, since it demonstrates that the full generality of weak inheritance effectively invalidates the current execution model (i.e. code generation principle). The problem is that _for one and the same object_ of type `d', when executing the do-part of `b' the meaning of `INNER' is not uniquely determined: We visit the do-part of `b' twice during the execution of an instance of `d', and the required meanings of `INNER' are different in the two cases. The code looks like this: ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE 'weakInherit'; -- program:descriptor -- (# o: (# t: ^text enter t[] do t[]->puttext #); (* Simulating the following patterns: * * a: (# do 'a'->o; inner; 'a'->o #); * b: a(# do 'b'->o; inner; 'b'->o #); * c: b!2(# do 'c'->o; outer; 'c'->o #); * d: c!2(# do 'd'->o; outer; 'd'->o #); * e: d(# do 'X'->o; #); *) a: _object(# init::< (# do (a_do##,2,1)->insert_dopart; INNER #); a_do: do_ptn(# do 'a'->o; do_inner; 'a'->o #)#); b: a (# init::< (# do (b_do##,3,1)->insert_dopart; INNER #); b_do: do_ptn(# do 'b'->o; do_inner; 'b'->o #)#); c: b(*!2*)(# init::< (# do (c_do##,4,2)->insert_dopart; INNER; #); c_do: do_ptn(# do 'c'->o; do_outer; 'c'->o #)#); d: c(*!2*)(# init::< (# do (d_do##,5,2)->insert_dopart; INNER #); d_do: do_ptn(# do 'd'->o; do_outer; 'd'->o #)#); e: d (# init::< (# do (e_do##,6,1)->insert_dopart; INNER #); e_do: do_ptn(# do 'X'->o #)#); do &a; newline; (* "aa" *) &b; newline; (* "abba" *) &c; newline; (* "acabbaca" *) &d; newline; (* "abdacabbacadba" *) &e; newline; (* "abdacabXbacadba" *) #) ---------------------------------------------------------------------- This example also illustrates that it is indeed possible to produce significant complexity by throwing in a few of the new language constructs. The last couple of examples illustrate some spurious possibilities delivered by the simulation implementation in standard BETA used above: Functional inheritance can be changed at run-time (dynamic inheritance): ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE 'weakInherit'; -- program:descriptor -- (# o: (# t: ^text enter t[] do t[]->puttext #); (* Simulating the following patterns: * * a: (# do 'a'->o; inner; 'a'->o #); * b: a(# do 'b'->o; inner; 'b'->o #); * c: b(# do 'c'->o; inner; 'c'->o #); * d: c(# do 'X'->o #); *) a: _object(# init::< (# do (a_do##,2,1)->insert_dopart; INNER #); a_do: do_ptn(# do 'a'->o; do_inner; 'a'->o #)#); b: a (# init::< (# do (b_do##,3,1)->insert_dopart; INNER #); b_do: do_ptn(# do 'b'->o; do_inner; 'b'->o #)#); c: b (# init::< (# do (c_do##,4,1)->insert_dopart; INNER #); c_do: do_ptn(# do 'c'->o; do_inner; 'c'->o #)#); d: c (# init::< (# do (d_do##,5,1)->insert_dopart; INNER #); d_do: do_ptn(# do 'X'->o #)#); theD: @d(# morph:levelObject(# do (d_do##,5,level)->insert_dopart #)#); do theD; newline; (* "abcXcba" *) 2->theD.morph; theD; newline; (* "abXba" *) 3->theD.morph; theD; newline; (* "aXa" *) 4->theD.morph; theD; newline; (* "X" *) #) ---------------------------------------------------------------------- Using a virtual pattern as a prefix is not yet supported. I think that a full implementation of general weak inheritance would as a side-effect make it easy to generate code for virtual prefixing. This is just a gut feeling of mine, and the following example is not by itself a convincing demonstration of it. Anyway, here it comes: ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE 'weakInherit'; -- program:descriptor -- (# o: (# t: ^text enter t[] do t[]->puttext #); (* Simulating the following patterns: * * a: (# v:< (# do '1'->o; INNER #); * do v(# do 'X'->o #) * #); * * b: a(# v:: (# do '2'->o #)#); *) a: (# v:< _object(# init::< (# do (v_do##,2,1)->insert_dopart; INNER #); v_do: do_ptn(# do '1'->o; do_inner #); #); do (# theV: @v; _do: theV.do_ptn(# do 'X'->o #); do theV.init; (_do##,theV.idt.top+1,1)->theV.insert_dopart; theV; #); #); b: a(# v::< (# init:: (# do (v2_do##,3,1)->insert_dopart #); v2_do: do_ptn(# do '2'->o; do_inner #); #); #); do &a; newline; (* "1X" *) &b; newline; (* "12X" *) #) ---------------------------------------------------------------------- The implementation: =================== There is a little fragment group supporting the actual compilation and execution of the above examples, and also serving to specify exactly what the semantics of general weak inheritance is. It looks like this: ---------------------------------------------------------------------- ORIGIN '~beta/basiclib/v1.4/betaenv'; INCLUDE '~beta/containers/v1.4/seqContainers'; (* ============================== THISISIT ============================== *) -- lib:attributes -- _object: (* plays the role normally played by 'object' *) (# <>; (* ---------- init ---------- *) init:< (# (* ----- initialize this object; only furtherbind this *) (* virtual, it is executed by the dopart when necessary *) <> #); (* ---------- operations ---------- *) insert_dopart: (# (* ----- insert new entry into IDT *) dopart: ##do_ptn; struc_level,func_gap: @integer; enter (dopart##,struc_level,func_gap) <> #); (* ---------- types ---------- *) do_ptn: (# (* ----- make the "dopart" a specialization of this *) do_inner: (* ----- use in place of the 'INNER' imperative *) (# <> #); do_outer: (* ----- use in place of the 'OUTER' imperative *) (# <> #); private: @<>; <> #); (* ---------- tracing ---------- *) trace_do:< (* ----- show entry/exit for doparts *) booleanObject; trace_insert:< (* ----- show IDT insertions *) booleanObject; trace_inners:< (* ----- show levels in inner stacks at creation *) booleanObject; (* ---------- private ---------- *) idt: @inner_dispatch_table; initialized: @boolean; <> #); (* ============================== IMPL ============================== *) -- objectLib:attributes -- levelObject: (# (* ----- like integerObject, but more mnemonic as used here *) level: @integer; enter level do INNER exit level #); i2lo: (# (* ----- convert integer to levelObject[] *) lo: ^levelObject; enter (&levelObject[]->lo[]).level exit lo[] #); dodo: (# (* execute the dopart of this object *) do (idt.top,NONE)->create_inners->do_inners; #); create_inners: (# (* create or complete an inner_stack *) inners: ^inner_stack; (* if entered<>NONE, expand this stack *) level: @integer; (* static specialization level *) enter (level,inners[]) do (if inners[]=NONE then (&inner_stack[]->inners[]).init; if); level->i2lo->inners.push; L: (# do level-(level->idt.at).gap->level->i2lo->inners.push; (if (level>1) then restart L if); #); inners[]->inners_trace; exit inners[] #); do_inners: (# (* execute the given dopart-stack *) inners,new_inners: ^inner_stack; idata: ^inner_data; dopart: ^do_ptn; level: @integer; enter inners[] do (if not inners.empty then inners.copy->new_inners[]; (new_inners.pop).level->level; level->idt.at->idata[]; &idata.dopart[]->dopart[]; new_inners[]->dopart.private._do_ptn_inners[]; level->dopart.private._do_ptn_level; dopart; if); #); inner_stack: stack (# element:: levelObject; print: (# str: ^stream; first: @boolean; enter str[] do (if str[]=NONE then screen[]->str[] if); true->first; scan (# do (if first then '{inners:'->str.puttext; false->first; else ','->str.put; if); current->str.putint; #); '}'->str.put; #); #); inner_data: (# (* an entry in the inner dispatch table *) dopart: ##do_ptn; gap: @integer; (* func-inheritance: how many levels skipped *) #); inner_dispatch_table: (# insert: (# idata: ^inner_data; pos: @integer; enter (idata[],pos) do (if pos>top then pos->top; (if top>contents.range then top->contents.extend if); if); idata[]->contents[pos][]; #); at: (# inx: @integer; enter inx exit contents[inx][] #); top: @integer; contents: [1] ^inner_data; #); create_inner: (# (* procedure which constructs IDT entry *) dopart: ##do_ptn; gap: @integer; idata: ^inner_data; enter (dopart##,gap) do &inner_data[]->idata[]; dopart##->idata.dopart##; gap->idata.gap; exit idata[] #); insert_trace: (# (* trace insertions into IDT *) struc_level,func_super_gap: @integer; enter (struc_level,func_super_gap) do (if trace_insert then '[insert:'->puttext; struc_level->putint; ','->puttext; func_super_gap->putint; ']'->puttext; if); #); inners_trace: (# (* trace creation of inner stacks *) inners: ^inner_stack; enter inners[] do (if trace_inners then screen[]->inners.print; if); exit inners[] #); pre_do_trace: levelObject (# (* trace initiations of dopart executions *) do (if trace_do then '['->puttext; (if level>0 then level->putint if); if); #); post_do_trace: levelObject (# (* trace terminations of dopart executions *) do (if trace_do then (if level>0 then level->putint if); ']'->puttext; if); #); object_dopart: do_ptn(# do do_inner #); -- initDopart:dopart -- do true->initialized; ((object_dopart##,1)->create_inner,1)->idt.insert; INNER; -- insertDopart:dopart -- do (struc_level,func_gap)->insert_trace; ((dopart##,func_gap)->create_inner,struc_level)->idt.insert; -- innerDopart:dopart -- do private._do_ptn_inners[]->do_inners; -- outerDopart:dopart -- do (private._do_ptn_level-1,private._do_ptn_inners.copy) ->create_inners ->do_inners; -- doptnDopart:dopart -- do private._do_ptn_level->pre_do_trace; INNER; private._do_ptn_level->post_do_trace; -- doPtnPrivate:descriptor -- (# (* using weird name to avoid name conflicts *) _do_ptn_inners: ^inner_stack; (* used to compute 'INNER' *) _do_ptn_level: @integer; (* static level of specialization *) #) -- objectDopart:dopart -- do (if not initialized then init if); dodo; ---------------------------------------------------------------------- What do you think? It's a bit fascinating, isn't it? -- Erik Ernst eernst@daimi.aau.dk Computer Science Department of Aarhus University, Denmark