From 3005cda5439326a89efd350754e392a2035cea10 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 6 Mar 2014 01:51:49 -0500 Subject: [PATCH] Support for stacked mixins The concept of stacked traits already existed in previous commits, but until now, mixins could not be stacked without some ugly errors. This also allows mixins to be stacked atop of themselves, duplicating their effect. This would naturally have limited use, but it's there. This differs slightly from Scala. For example, consider this ease.js mixin: C.use( T ).use( T )() This is perfectly valid---it has the effect of stacking T twice. In reality, ease.js is doing this: - C' = C.use( T ); - new C'.use( T ); That is, it each call to `use' creates another class with T mixed in. Scala, on the other hand, complains in this situation: new C with T with T will produce an error stating that "trait T is inherited twice". You can work around this, however, by doing this: class Ca extends T new Ca with T In fact, this is precisely what ease.js is doing, as mentioned above; the "use.use" syntax is merely shorthand for this: new C.use( T ).extend( {} ).use( T ) Just keep that in mind. --- lib/MemberBuilder.js | 2 +- lib/Trait.js | 49 +++++++++++++------- lib/class.js | 2 +- test/Trait/LinearizationTest.js | 80 +++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 19 deletions(-) diff --git a/lib/MemberBuilder.js b/lib/MemberBuilder.js index ab568e6..a184d64 100644 --- a/lib/MemberBuilder.js +++ b/lib/MemberBuilder.js @@ -226,7 +226,7 @@ function aoverride( name ) { return function() { - return this.___$$pmo$$.___$$parent$$[ name ] + return this.___$$super$$.prototype[ name ] .apply( this.___$$pmo$$, arguments ); }; } diff --git a/lib/Trait.js b/lib/Trait.js index ff335a7..279297e 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -119,9 +119,9 @@ Trait.extend = function( dfn ) } // invoked to trigger mixin - TraitType.__mixin = function( dfn, tc ) + TraitType.__mixin = function( dfn, tc, base ) { - mixin( TraitType, dfn, tc ); + mixin( TraitType, dfn, tc, base ); }; // mixes in implemented types @@ -178,10 +178,14 @@ function createConcrete( acls ) // protected member object (we define this as protected so that the // parent ACLS has access to it (!), which is not prohibited since // JS does not provide a strict typing mechanism...this is a kluge) + // and target supertype---that is, what __super calls should + // referene 'protected ___$$pmo$$': null, - __construct: function( pmo ) + 'protected ___$$super$$': null, + __construct: function( base, pmo ) { - this.___$$pmo$$ = pmo; + this.___$$super$$ = base; + this.___$$pmo$$ = pmo; }, // mainly for debugging; should really never see this. @@ -289,16 +293,17 @@ function createVirtProxy( acls, dfn ) * @param {Trait} trait trait to mix in * @param {Object} dfn definition object to merge into * @param {Array} tc trait class context + * @param {Class} base target supertyep * * @return {Object} dfn */ -function mixin( trait, dfn, tc ) +function mixin( trait, dfn, tc, base ) { // the abstract class hidden within the trait var acls = trait.__acls; // retrieve the private member name that will contain this trait object - var iname = addTraitInst( trait, dfn, tc ); + var iname = addTraitInst( trait, dfn, tc, base ); // recursively mix in trait's underlying abstract class (ensuring that // anything that the trait inherits from is also properly mixed in) @@ -461,15 +466,21 @@ function mixMethods( src, dest, vis, iname ) * Here, `tc' and `to' are understood to be, respectively, ``trait class'' * and ``trait object''. * - * @param {Class} T trait - * @param {Object} dfn definition object of class being mixed into - * @param {Array} tc trait class object + * @param {Class} T trait + * @param {Object} dfn definition object of class being mixed into + * @param {Array} tc trait class object + * @param {Class} base target supertyep * * @return {string} private member into which C instance shall be stored */ -function addTraitInst( T, dfn, tc ) +function addTraitInst( T, dfn, tc, base ) { - var iname = '___$to$' + T.__acls.__cid; + var base_cid = base.__cid; + + // creates a property of the form ___$to$N$M to hold the trait object + // reference; M is required because of the private member restrictions + // imposed to be consistent with pre-ES5 fallback + var iname = '___$to$' + T.__acls.__cid + '$' + base_cid; // the trait object array will contain two values: the destination field // and the trait to instantiate @@ -486,7 +497,7 @@ function addTraitInst( T, dfn, tc ) // definition (this prevents warnings if there is not a supertype // that defines the trait ctor) dfn[ 'weak virtual ___$$tctor$$' ] = function() {}; - dfn[ 'virtual override ___$$tctor$$' ] = createTctor( tc ); + dfn[ 'virtual override ___$$tctor$$' ] = createTctor( tc, base ); } return iname; @@ -504,9 +515,12 @@ function addTraitInst( T, dfn, tc ) * This will lazily create the concrete trait class if it does not already * exist, which saves work if the trait is never used. * + * @param {Object} tc trait class list + * @param {Class} base target supertype + * * @return {undefined} */ -function tctor( tc ) +function tctor( tc, base ) { // instantiate all traits and assign the object to their // respective fields @@ -521,7 +535,7 @@ function tctor( tc ) // (but not private); in return, we will use its own protected // visibility object to gain access to its protected members...quite // the intimate relationship - this[ f ] = C( this.___$$vis$$ ).___$$vis$$; + this[ f ] = C( base, this.___$$vis$$ ).___$$vis$$; } // if we are a subtype, be sure to initialize our parent's traits @@ -535,15 +549,16 @@ function tctor( tc ) * This binds the generic trait constructor to a reference to the provided * trait class list. * - * @param {Object} tc trait class list + * @param {Object} tc trait class list + * @param {Class} base target supertype * * @return {function()} trait constructor */ -function createTctor( tc ) +function createTctor( tc, base ) { return function() { - return tctor.call( this, tc ); + return tctor.call( this, tc, base ); }; } diff --git a/lib/class.js b/lib/class.js index 9bd0c92..6454a2c 100644 --- a/lib/class.js +++ b/lib/class.js @@ -429,7 +429,7 @@ function createMixedClass( base, traits ) // "mix" each trait into the class definition object for ( var i = 0, n = traits.length; i < n; i++ ) { - traits[ i ].__mixin( dfn, tc ); + traits[ i ].__mixin( dfn, tc, ( base || ClassBuilder.ClassBase ) ); } // create the mixed class from the above generated definition diff --git a/test/Trait/LinearizationTest.js b/test/Trait/LinearizationTest.js index 3906bd1..579751b 100644 --- a/test/Trait/LinearizationTest.js +++ b/test/Trait/LinearizationTest.js @@ -119,5 +119,85 @@ require( 'common' ).testCase( this.assertStrictEqual( C.use( T )().foo(), expected ); }, + + + /** + * Similar in spirit to the previous test: a supertype with a mixin + * should be treated just as any other class. + * + * Another way of phrasing this test is: "traits are stackable". + * Importantly, this also means that `virtual' must play nicely with + * `abstract override'. + */ + 'Mixin overriding another mixin method M has super method M': function() + { + var called = {}; + + var I = this.Interface( { foo: [] } ); + + var Ta = this.Sut.implement( I ).extend( + { + 'virtual abstract override foo': function() + { + called.a = true; + this.__super(); + }, + } ); + + var Tb = this.Sut.implement( I ).extend( + { + 'abstract override foo': function() + { + called.b = true; + this.__super(); + }, + } ); + + this.Class.implement( I ).extend( + { + 'virtual foo': function() { called.base = true; }, + } ).use( Ta ).use( Tb )().foo(); + + this.assertOk( called.a ); + this.assertOk( called.b ); + this.assertOk( called.base ); + }, + + + /** + * Essentially the same as the above test, but ensures that a mixin can + * be stacked multiple times atop of itself with no ill effects. We + * assume that all else is working (per the previous test). + * + * The number of times we stack the mixin is not really relevant, so + * long as it is >= 2; we did 3 here just for the hell of it to + * demonstrate that there is ideally no limit. + */ + 'Mixin can be mixed in atop of itself': function() + { + var called = 0, + calledbase = false; + + var I = this.Interface( { foo: [] } ); + + var T = this.Sut.implement( I ).extend( + { + 'virtual abstract override foo': function() + { + called++; + this.__super(); + }, + } ); + + this.Class.implement( I ).extend( + { + 'virtual foo': function() { calledbase = true; }, + } ).use( T ).use( T ).use( T )().foo(); + + + // mixed in thrice, so it should have stacked thrice + this.assertEqual( called, 3 ); + this.assertOk( calledbase ); + }, } );