diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index 11bda6b..06bac0b 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -72,6 +72,7 @@ var util = require( './util' ), */ public_methods = { '__construct': true, + '__mixin': true, 'toString': true, '__toString': true, }, @@ -879,12 +880,17 @@ exports.prototype.createConcreteCtor = function( cname, members ) // generate and store unique instance id attachInstanceId( this, ++_self._instanceId ); - // handle internal trait initialization logic, if provided - if ( typeof this.___$$tctor$$ === 'function' ) + // FIXME: this is a bit of a kluge for determining whether the ctor + // should be invoked before a child prector... + var haspre = ( typeof this.___$$ctor$pre$$ === 'function' ); + if ( haspre + && ClassInstance.prototype.hasOwnProperty( '___$$ctor$pre$$' ) + ) { // FIXME: we're exposing _priv to something that can be - // malicously set by the user; encapsulate tctor - this.___$$tctor$$.call( this, _priv ); + // malicously set by the user + this.___$$ctor$pre$$( _priv ); + haspre = false; } // call the constructor, if one was provided @@ -896,6 +902,17 @@ exports.prototype.createConcreteCtor = function( cname, members ) this.__construct.apply( this, ( args || arguments ) ); } + // FIXME: see above + if ( haspre ) + { + this.___$$ctor$pre$$( _priv ); + } + + if ( typeof this.___$$ctor$post$$ === 'function' ) + { + this.___$$ctor$post$$( _priv ); + } + args = null; // attach any instance properties/methods (done after diff --git a/lib/Trait.js b/lib/Trait.js index 4bd17c2..0cca02f 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -23,6 +23,10 @@ var AbstractClass = require( './class_abstract' ), ClassBuilder = require( './ClassBuilder' ), Interface = require( './interface' ); + +function _fvoid() {}; + + /** * Trait constructor / base object * @@ -100,7 +104,9 @@ Trait.extend = function( dfn ) // store any provided name, since we'll be clobbering it (the definition // object will be used to define the hidden abstract class) - var name = dfn.__name || '(Trait)'; + var name = dfn.__name || '(Trait)', + type = _getTraitType( dfn ), + isparam = ( type === 'param' ); // augment the parser to handle our own oddities dfn.___$$parser$$ = { @@ -116,10 +122,41 @@ Trait.extend = function( dfn ) // give the abstract trait class a distinctive name for debugging dfn.__name = '#AbstractTrait#'; - function TraitType() - { - throw Error( "Cannot instantiate trait" ); - }; + // if __mixin was provided,in the definition, then we should crate a + // paramaterized trait + var Trait = ( isparam ) + ? function ParameterTraitType() + { + // duplicate ars in a way that v8 can optimize + var args = [], i = arguments.length; + while ( i-- ) args[ i ] = arguments[ i ]; + + var AT = function ArgumentTrait() + { + throw Error( "Cannot re-configure argument trait" ); + }; + + // TODO: mess! + AT.___$$mixinargs = args; + AT.__trait = 'arg'; + AT.__acls = Trait.__acls; + AT.__ccls = Trait.__ccls; + AT.toString = Trait.toString; + AT.__mixinImpl = Trait.__mixinImpl; + AT.__isInstanceOf = Trait.__isInstanceOf; + + // mix in the argument trait instead of the original + AT.__mixin = function( dfn, tc, base ) + { + mixin( AT, dfn, tc, base ); + }; + + return AT; + } + : function TraitType() + { + throw Error( "Cannot instantiate non-parameterized trait" ); + }; // implement interfaces if indicated var base = AbstractClass; @@ -131,33 +168,50 @@ Trait.extend = function( dfn ) // and here we can see that traits are quite literally abstract classes var tclass = base.extend( dfn ); - TraitType.__trait = true; - TraitType.__acls = tclass; - TraitType.__ccls = null; - TraitType.toString = function() + Trait.__trait = type; + Trait.__acls = tclass; + Trait.__ccls = null; + Trait.toString = function() { return ''+name; }; // invoked to trigger mixin - TraitType.__mixin = function( dfn, tc, base ) + Trait.__mixin = function( dfn, tc, base ) { - mixin( TraitType, dfn, tc, base ); + mixin( Trait, dfn, tc, base ); }; // mixes in implemented types - TraitType.__mixinImpl = function( dest_meta ) + Trait.__mixinImpl = function( dest_meta ) { mixinImpl( tclass, dest_meta ); }; // TODO: this and the above should use util.defineSecureProp - TraitType.__isInstanceOf = Interface.isInstanceOf; + Trait.__isInstanceOf = Interface.isInstanceOf; - return TraitType; + return Trait; }; +/** + * Retrieve a string representation of the trait type + * + * A trait is parameterized if it has a __mixin method; otherwise, it is + * standard. + * + * @param {Object} dfn trait definition object + * @return {string} trait type + */ +function _getTraitType( dfn ) +{ + return ( typeof dfn.__mixin === 'function' ) + ? 'param' + : 'std'; +} + + /** * Verifies trait member restrictions * @@ -295,8 +349,7 @@ function createImplement( ifaces, name ) /** * Determines if the provided value references a trait * - * @param {*} trait value to check - * + * @param {*} trait value to check * @return {boolean} whether the provided value references a trait */ Trait.isTrait = function( trait ) @@ -305,6 +358,32 @@ Trait.isTrait = function( trait ) }; +/** + * Determines if the provided value references a parameterized trait + * + * @param {*} trait value to check + * @return {boolean} whether the provided value references a param trait + */ +Trait.isParameterTrait = function( trait ) +{ + return !!( ( trait || {} ).__trait === 'param' ); +}; + + +/** + * Determines if the provided value references an argument trait + * + * An argument trait is a configured parameter trait. + * + * @param {*} trait value to check + * @return {boolean} whether the provided value references an arg trait + */ +Trait.isArgumentTrait = function( trait ) +{ + return !!( ( trait || {} ).__trait === 'arg' ); +}; + + /** * Create a concrete class from the abstract trait class * @@ -460,6 +539,24 @@ function mixin( trait, dfn, tc, base ) // retrieve the private member name that will contain this trait object var iname = addTraitInst( trait, dfn, tc, base ); + // TODO: this should not be necessary for dfn metadata + dfn[ 'weak virtual ___$$ctor$pre$$' ] = _fvoid; + dfn[ 'weak virtual ___$$ctor$post$$' ] = _fvoid; + + // TODO: this is a kluge; generalize and move + // this ensures __construct is called before __mixin when mixing into + // the base class + if ( base === ClassBuilder.ClassBase ) + { + dfn[ 'virtual override ___$$ctor$post$$' ] = _tctorApply; + dfn[ 'virtual override ___$$ctor$pre$$' ] = _fvoid; + } + else + { + dfn[ 'virtual override ___$$ctor$post$$' ] = _fvoid; + dfn[ 'virtual override ___$$ctor$pre$$' ] = _tctorApply; + } + // recursively mix in trait's underlying abstract class (ensuring that // anything that the trait inherits from is also properly mixed in) mixinCls( acls, dfn, iname ); @@ -542,6 +639,14 @@ function mixMethods( src, dest, vis, iname ) continue; } + // TODO: generalize + // __mixin is exclusive to the trait (private-ish, but can be + // invoked publically internally) + if ( f === '__mixin' ) + { + continue; + } + // TODO: this is a kluge; we'll use proper reflection eventually, // but for now, this is how we determine if this is an actual method // vs. something that just happens to be on the visibility object @@ -670,6 +775,8 @@ function addTraitInst( T, dfn, tc, base ) * resulting objects assigned to their rsepective pre-determined field * names. * + * The MIXINARGS are only useful in the case of parameterized trait. + * * This will lazily create the concrete trait class if it does not already * exist, which saves work if the trait is never used. * @@ -701,6 +808,12 @@ function tctor( tc, base, privsym ) // visibility object to gain access to its protected members...quite // the intimate relationship this[ f ] = C( base, this[ privsym ].vis )[ privsym ].vis; + + // this has been previously validated to ensure that it is a + // function + this[ f ].__mixin && this[ f ].__mixin.apply( + this[ f ], T.___$$mixinargs + ); } // if we are a subtype, be sure to initialize our parent's traits @@ -728,5 +841,11 @@ function createTctor( tc, base ) } +function _tctorApply() +{ + this.___$$tctor$$.apply( this, arguments ); +} + + module.exports = Trait; diff --git a/test/Trait/DefinitionTest.js b/test/Trait/DefinitionTest.js index 5f14802..df6ba8f 100644 --- a/test/Trait/DefinitionTest.js +++ b/test/Trait/DefinitionTest.js @@ -82,6 +82,8 @@ require( 'common' ).testCase( /** * A trait can only be used by something else---it does not make sense * to instantiate them directly, since they form an incomplete picture. + * + * Now, that said, see parameterized traits. */ '@each(ctor) Cannot instantiate trait without error': function( T ) { diff --git a/test/Trait/ParameterTest.js b/test/Trait/ParameterTest.js new file mode 100644 index 0000000..077131a --- /dev/null +++ b/test/Trait/ParameterTest.js @@ -0,0 +1,399 @@ +/** + * Tests parameterized traits + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/*** XXX __construct or __mixin first? __mixin with no parameters should + * permit standard trait with initialization procedure ***/ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'Trait' ); + this.Class = this.require( 'class' ); + + var _self = this; + this.createParamTrait = function( f ) + { + return _self.Sut( { __mixin: ( f || function() {} ) } ); + }; + }, + + + /** + * Since traits are reusable components mixed into classes, they + * themselves do not have a constructor. This puts the user at a + * disadvantage, because she would have to create a new trait to simply + * to provide some sort of configuration at the time the class is + * instantiated. Adding a method to do the configuration is another + * option, but that is inconvenient, especially when the state is + * intended to be immutable. + * + * This does not suffer from the issue that Scala is having in trying to + * implement a similar feature because traits cannot have non-private + * properties; the linearization process disambiguates. + * + * When a trait contains a __mixin method, it is created as a + * ParameterTraitType instead of a TraitType. Both must be recognized as + * traits so that they can both be mixed in as expected; a method is + * provided to assert whether or not a trait is a parameter trait + * programatically, since attempting to configure a non-param trait will + * throw an exception. + */ + 'Can create parameter traits': function() + { + var T = this.createParamTrait(); + + this.assertOk( this.Sut.isParameterTrait( T ) ); + this.assertOk( this.Sut.isTrait( T ) ); + }, + + + /** + * A parameter trait is in an uninitialized state---it cannot be mixed + * in until arguments have been provided; same rationale as a class + * constructor. + */ + 'Cannot mix in a parameter trait': function() + { + var _self = this; + this.assertThrows( function() + { + _self.Class.use( _self.createParamTrait() )(); + } ); + }, + + + /** + * Invoking a parameter trait will produce an argument trait which may + * be mixed in. This has the effect of appearing as though the trait is + * being instantiated (but it's not). + */ + 'Invoking parameter trait produces argument trait': function() + { + var _self = this; + this.assertDoesNotThrow( function() + { + _self.assertOk( + _self.Sut.isArgumentTrait( _self.createParamTrait()() ) + ); + } ); + }, + + + /** + * Traits cannot be instantiated; ensure that this remains true, even + * with the parameterized trait implementation. + */ + 'Invoking a standard trait throws an exception': function() + { + var Sut = this.Sut; + this.assertThrows( function() + { + // no __mixin method; not a param trait + Sut( {} )(); + } ); + }, + + + /** + * Argument traits can be mixed in just as non-parameterized traits can; + * it would be silly not to consider them to be traits through our + * reflection API. + */ + 'Recognizes argument trait as a trait': function() + { + this.assertOk( + this.Sut.isTrait( this.createParamTrait()() ) + ); + }, + + + /** + * A param trait, upon configuration, returns an immutable argument + * trait; any attempt to invoke it (e.g. to try to re-configure) is in + * error. + */ + 'Cannot re-configure argument trait': function() + { + var _self = this; + this.assertThrows( function() + { + // ParameterTrait => ArgumentTrait => Error + _self.createParamTrait()()(); + } ); + }, + + + /** + * Upon instantiating a class into which an argument trait was mixed, + * all configuration arguments should be passed to the __mixin method. + * Note that this means that __mixin *will not* be called at the point + * that the param trait is configured. + */ + '__mixin is invoked upon class instantiation': function() + { + var called = 0; + var T = this.createParamTrait( function() + { + called++; + } ); + + // ensure we only invoke __mixin a single time + this.Class( {} ).use( T() )(); + this.assertEqual( called, 1 ); + }, + + + /** + * Standard sanity check---make sure that the arguments provided during + * configuration are passed as-is, by reference, to __mixin. Note that + * this has the terrible consequence that, should one of the arguments + * be modified by __mixin (e.g. an object field), then it will be + * modified for all other __mixin calls. But that is the case with any + * function. ;) + */ + '__mixin is passed arguments by reference': function() + { + var args, + a = { a: 'a' }, + b = { b: 'b' }; + + var T = this.createParamTrait( function() + { + args = arguments; + } ); + + this.Class( {} ).use( T( a, b ) )(); + + this.assertStrictEqual( a, args[ 0 ] ); + this.assertStrictEqual( b, args[ 1 ] ); + }, + + + /** + * The __mixin method should be invoked within the context of the trait + * and should therefore have access to its private members. Indeed, + * parameterized traits would have far more limited use if __mixin did + * not have access to private members, because that would be the proper + * place to hold configuration data. + */ + '__mixin has access to trait private members': function() + { + var expected = {}; + + var T = this.Sut( + { + 'private _foo': null, + __mixin: function( arg ) { this._foo = arg; }, + getFoo: function() { return this._foo; }, + } ); + + this.assertStrictEqual( expected, + this.Class( {} ).use( T( expected ) )().getFoo() + ); + }, + + + /** + * It is still useful to be able to define a __mixin method to be called + * as an initialization method for default state; otherwise, arbitrary + * method overrides or explicit method calls are needed. + */ + '__mixin with empty parameter list is still invoked': function() + { + var expected = {}, + given; + + var T = this.createParamTrait( function() { given = expected; } ); + + // notice that we still configure T, with an empty argument list + this.Class( {} ).use( T() )(); + this.assertStrictEqual( expected, given ); + }, + + + /** + * Parameterized traits are intended to be configured. However, there + * are a number of reasons to allow them to be mixed in without + * configuration (that is---without being converted into argument + * traits): + * - Permits default behavior with no configuration, overridable with; + * - If any __mixin definition required configuration, then traits + * would break backwards-compatibility if they wished to define it, + * with no means of maintaining BC; + * - Allows trait itself to determine whether arguments are required. + */ + 'Mixing in param trait will invoke __mixin with no arguments': + function() + { + var n = 0; + + // ensure consistency at any arity; we'll test nullary and unary, + // assuming the same holds true for any n-ary __mixin method + var T0 = this.createParamTrait( function() { n |= 1; } ), + T1 = this.createParamTrait( function( a ) { n |= 2; } ); + + // ensure that param traits do not throw errors when mixed in (as + // opposed to argument traits, which have been tested thusfar) + var C = this.Class( {} ); + this.assertDoesNotThrow( function() + { + C.use( T0 )(); + C.use( T1 )(); + } ); + + this.assertEqual( n, 3 ); + }, + + + /** + * Sibling traits are an interesting case---rather than stacking, they + * are mixed in alongside each other, meaning that there may be + * multiple traits that define __mixin. Ordinarily, this is a problem; + * however, __mixin shall be treated as if it were private and shall be + * invoked once per trait, giving each a chance to initialize. + * + * Furthermore, each should retain access to their own configuration. + */ + 'Invokes __mixin of each sibling mixin': function() + { + var args = [], + vals = [ {}, [] ], + c = function() { args.push( arguments ) }; + + var Ta = this.createParamTrait( c ), + Tb = this.createParamTrait( c ); + + this.Class( {} ).use( Ta( vals[0] ), Tb( vals[1] ) )(); + + this.assertEqual( args.length, 2 ); + this.assertStrictEqual( args[0][0], vals[0] ); + this.assertStrictEqual( args[1][0], vals[1] ); + }, + + + /** + * This decision is not arbitrary. + * + * We shall consider two different scenarios: first, the case of mixing + * in some trait T atop of some class C. Assume that C defines a + * __construct method; it does not know whether or not a trait will be + * mixed in, nor should it care---it should proceed initializing its + * state as normal. However, what if a trait were to be mixed in, + * overriding certain behaviors? It is then imperative that T be + * initialized prior to any calls by C#__construct. It is not important + * that C be initialized prior to T#__mixin, because T can know that it + * should not invoke any methods that will fail---it should be used only + * to initialize state. (In the future, ease.js may enforce this + * restriction.) + * + * The second scenario is described in the test that follows. + */ + 'Invokes __mixin before __construct when C.use(T)': function() + { + var mixok = false; + + var T = this.createParamTrait( function() { mixok = true } ), + C = this.Class( + { + __construct: function() + { + if ( !mixok ) throw Error( + "__construct called before __mixin" + ); + } + } ); + + this.assertDoesNotThrow( function() + { + C.use( T )(); + } ); + }, + + + /** + * (Continued from above test.) + * + * In the reverse situation---whereby C effectively extends T---we want + * __construct to instead be called *after* __mixin of T (and any other + * traits in the set). This is because __construct may wish to invoke + * methods of T, but what would cause problems if T were not + * initialized. Further, T would not have knowledge of C and, if it + * expected a concrete implementation to be called from T#__mixin, then + * T would have already been initialized, or C's concrete implementation + * would know what not to do (in the case of a partial initialization). + * + * This is also more intuitive---we are invoking initialize methods as + * if they were part of a stack. + */ + 'Invokes __construct before __mixin when Class.use(T).extend()': + function() + { + var cok = false; + + var T = this.createParamTrait( function() + { + if ( !cok ) throw Error( + "__mixin called before __construct" + ); + } ); + + var C = this.Class.use( T ).extend( + { + __construct: function() { cok = true } + } ); + + this.assertDoesNotThrow( function() + { + C(); + } ); + }, + + + /** + * The same concept as above, extended to subtypes. In particular, we + * need to ensure that the subtype is able to properly initialize or + * alter state that __mixin of a supertype depends upon. + */ + 'Subtype invokes ctor before supertype __construct or __mixin': + function() + { + var cok = false; + + var T = this.createParamTrait( function() + { + if ( !cok ) throw Error( + "__mixin called before Sub#__construct" + ); + } ); + + var Sub = this.Class( {} ).use( T ).extend( + { + __construct: function() { cok = true } + } ); + + this.assertDoesNotThrow( function() + { + Sub(); + } ); + }, +} ); +