/** * Provides system for code reuse via traits * * Copyright (C) 2014, 2015 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 . */ var AbstractClass = require( './class_abstract' ), ClassBuilder = require( './ClassBuilder' ), Interface = require( './interface' ); function _fvoid() {}; /** * Trait constructor / base object * * The interpretation of the argument list varies by number. Further, * various trait methods may be used as an alternative to invoking this * constructor. * * @return {Function} trait */ function Trait() { switch ( arguments.length ) { case 0: throw Error( "Missing trait name or definition" ); case 1: return ( typeof arguments[ 0 ] === 'string' ) ? _createStaging.apply( this, arguments ) : Trait.extend.apply( this, arguments ); case 2: return createNamedTrait.apply( this, arguments ); } throw Error( "Expecting at most two arguments for definition of named " + "Trait " + name + "'; " + arguments.length + " given" ); }; /** * Create a named trait * * @param {string} name trait name * @param {Object} def trait definition * * @return {Function} named trait */ function createNamedTrait( name, dfn ) { if ( typeof name !== 'string' ) { throw Error( "First argument of named class definition must be a string" ); } dfn.__name = name; return Trait.extend( dfn ); } function _createStaging( name ) { return { extend: function( dfn ) { return createNamedTrait( name, dfn ); }, implement: function() { return createImplement( arguments, name ); }, }; } Trait.extend = function( /* ... */ ) { var an = arguments.length, dfn = arguments[ an - 1 ], has_ext_base = ( an > 1 ), ext_base = ( has_ext_base ) ? arguments[ 0 ] : null; if ( an > 2 ) { throw Error( "Unexpected number of arguments to Trait.extend" ); } if ( has_ext_base ) { var basetype = typeof ext_base; if ( ( ext_base === null ) || !( ( basetype === 'object' ) || ( basetype === 'function' ) ) ) { throw TypeError( "Trait cannot extend base of type '" + basetype + "'" ); } // prevent extending final classes (TODO: abstract this check; see // also ClassBuilder) if ( ext_base.___$$final$$ === true ) { throw TypeError( "Trait cannot extend final class" ); } // TODO: this is intended to be temporary; see Trait/ClassExtendTest if ( module.exports.isTrait( ext_base ) ) { throw TypeError( "Traits cannot extend other traits" ); } } // we may have been passed some additional metadata var meta = ( this || {} ).__$$meta || {}; // 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)', type = _getTraitType( dfn ), isparam = ( type === 'param' ); // augment the parser to handle our own oddities dfn.___$$parser$$ = { each: _parseMember, property: _parseProps, getset: _parseGetSet, }; // automatically mark ourselves as abstract if an abstract method is // provided dfn.___$$auto$abstract$$ = true; // give the abstract trait class a distinctive name for debugging dfn.__name = '#AbstractTrait#'; // 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; if ( meta.ifaces ) { base = base.implement.apply( null, meta.ifaces ); } // and here we can see that traits are quite literally abstract classes var tclass = ( ext_base ) ? base.extend( ext_base, dfn ) : base.extend( dfn ); Trait.__trait = type; Trait.__acls = tclass; Trait.__ccls = null; Trait.__extbase = ext_base; Trait.toString = function() { return ''+name; }; // we're not a param trait, but we want consistent data Trait.___$$mixinargs = []; // invoked to trigger mixin Trait.__mixin = function( dfn, tc, base ) { mixin( Trait, dfn, tc, base ); }; // mixes in implemented types Trait.__mixinImpl = function( dest_meta ) { mixinImpl( tclass, dest_meta ); }; // TODO: this and the above should use util.defineSecureProp Trait.__isInstanceOf = Interface.isInstanceOf; return Trait; }; /** * Validate whether mixin is permitted * * If a mixee (the trait being mixed in) extends some type S, then a * contract has been created mandating that that trait may only be mixed * into something of type S; a `TypeError` will be thrown if this contract * is violated. * * @param {Class} base mixer (target of mixin) * @param {Trait} T mixee (trait being mixed in) * * @return {undefined} * * @throws {TypeError} on type contract violation */ function _validateMixin( base, T ) { if ( !T.__extbase ) { return; } // TODO: isSubtypeOf if ( !( ( T.__extbase === base ) || ClassBuilder.isInstanceOf( T.__extbase, base.asPrototype() ) ) ) { throw TypeError( "Cannot mix trait " + T.toString() + " into " + base.toString() + "; mixer must be of type " + T.__extbase.toString() ); } } /** * 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 * * @param {string} name property name * @param {*} value property value * @param {Object} keywords property keywords * @param {Function} h original handler that we replaced * * @return {undefined} */ function _parseMember( name, value, keywords, h ) { // traits are not permitted to define constructors if ( name === '__construct' ) { throw Error( "Traits may not define __construct" ); } // will be supported in future versions if ( keywords['static'] ) { throw Error( "Cannot define member `" + name + "'; static trait " + "members are currently unsupported" ); } // apply original handler h.apply( this, arguments ); } /** * Throws error if non-internal property is found within PROPS * * For details and rationale, see the Trait/PropertyTest case. * * @param {string} name property name * @param {*} value property value * @param {Object} keywords property keywords * @param {Function} h original handler that we replaced * * @return {undefined} */ function _parseProps( name, value, keywords, h ) { // ignore internal properties if ( name.substr( 0, 3 ) === '___' ) { return; } if ( !( keywords['private'] ) ) { throw Error( "Cannot define property `" + name + "'; only private " + "properties are permitted within Trait definitions" ); } // apply original handler h.apply( this, arguments ); } /** * Immediately throws an exception, as getters/setters are unsupported * * This is a temporary restriction; they will be supported in future * releases. * * @param {string} name property name * @param {*} value property value * @param {Object} keywords property keywords * @param {Function} h original handler that we replaced * * @return {undefined} */ function _parseGetSet( name, value, keywords, h ) { throw Error( "Cannot define property `" + name + "'; getters/setters are " + "currently unsupported" ); } /** * Implement one or more interfaces * * Implementing an interface into a trait has the same effect as it does * within classes in that it will automatically define abstract methods * unless a concrete method is provided. Further, the class that the trait * is mixed into will act as though it implemented the interfaces. * * @param {...Function} interfaces interfaces to implement * * @return {Object} staged trait object */ Trait.implement = function() { return createImplement( arguments ); }; /** * Create a staging object from which a trait implementing a set of * interfaces may be defined * * @param {...Function} interfaces interfaces to implement * @param {string=} name optional trait name * * @return {Object} staged trait object */ function createImplement( ifaces, name ) { return { extend: function( dfn ) { if ( name ) { dfn.__name = name; } // pass our interface metadata as the invocation context return Trait.extend.call( { __$$meta: { ifaces: ifaces } }, dfn ); }, }; } /** * Determines if the provided value references a trait * * @param {*} trait value to check * @return {boolean} whether the provided value references a trait */ Trait.isTrait = function( trait ) { return !!( trait || {} ).__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 * * This class is the one that will be instantiated by classes that mix in * the trait. * * @param {AbstractClass} acls abstract trait class * * @return {Class} concrete trait class for instantiation */ function createConcrete( acls ) { // start by providing a concrete implementation for our dummy method and // a constructor that accepts the protected member object of the // containing class var dfn = { // 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, 'protected ___$$super$$': null, __construct: function( base, pmo ) { this.___$$super$$ = base; this.___$$pmo$$ = pmo; }, // mainly for debugging; should really never see this. __name: '#ConcreteTrait#', }; // every abstract method should be overridden with a proxy to the // protected member object that will be passed in via the ctor var amethods = ClassBuilder.getMeta( acls ).abstractMethods; for ( var f in amethods ) { // TODO: would be nice if this check could be for '___'; need to // replace amethods.__length with something else, then if ( !( Object.hasOwnProperty.call( amethods, f ) ) || ( f.substr( 0, 2 ) === '__' ) ) { continue; } // we know that if it's not public, then it must be protected var vis = ( acls.___$$methods$$['public'][ f ] !== undefined ) ? 'public' : 'protected'; // setting the correct visibility modified is important to prevent // visibility de-escalation errors if a protected concrete method is // provided dfn[ vis + ' proxy ' + f ] = '___$$pmo$$'; } // virtual methods need to be handled with care to ensure that we invoke // any overrides createVirtProxy( acls, dfn ); return acls.extend( dfn ); } /** * Create virtual method proxies for all virtual members * * Virtual methods are a bit of hassle with traits: we are in a situation * where we do not know at the time that the trait is created whether or not * the virtual method has been overridden, since the class that the trait is * mixed into may do the overriding. Therefore, we must check if an override * has occured *when the method is invoked*; there is room for optimization * there (by making such a determination at the time of mixin), but we'll * leave that for later. * * @param {AbstractClass} acls abstract trait class * @param {Object} dfn destination definition object * * @return {undefined} */ function createVirtProxy( acls, dfn ) { var vmembers = ClassBuilder.getMeta( acls ).virtualMembers; // f = `field' for ( var f in vmembers ) { var vis = ( acls.___$$methods$$['public'][ f ] !== undefined ) ? 'public' : 'protected'; var srcmethod = acls.___$$methods$$[ vis ][ f ], plen = srcmethod.__length; // this is the aforementioned proxy method; see the docblock for // more information dfn[ vis + ' virtual override ' + f ] = ( function( f ) { var retf = function() { var pmo = this.___$$pmo$$, o = pmo[ f ]; // proxy to virtual override from the class we are mixed // into, if found; otherwise, proxy to our supertype return ( o ) ? o.apply( pmo, arguments ) : this.__super.apply( this, arguments ); }; retf.__length = plen; return retf; } )( f ); // this guy bypasses the above virtual override check, which is // necessary in certain cases to prevent infinte recursion dfn[ vis + ' virtual __$$' + f ] = ( function( method ) { var retf = function() { return method.apply( this, arguments ); }; retf.__length = plen; return retf; } )( srcmethod ); } } /** * Mix trait into the given definition * * The original object DFN is modified; it is not cloned. TC should be * initialized to an empty array; it is used to store context data for * mixing in traits and will be encapsulated within a ctor closure (and thus * will remain in memory). * * @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 supertype * * @return {Object} dfn */ function mixin( trait, dfn, tc, base ) { _validateMixin( base, trait ); // 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, 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 ); return dfn; } /** * Recursively mix in class methods * * If CLS extends another class, its methods will be recursively processed * to ensure that the entire prototype chain is properly proxied. * * For an explanation of the iname parameter, see the mixin function. * * @param {Class} cls class to mix in * @param {Object} dfn definition object to merge into * @param {string} iname trait object private member instance name * * @return {undefined} */ function mixinCls( cls, dfn, iname, inparent ) { var methods = cls.___$$methods$$; mixMethods( methods['public'], dfn, 'public', iname, inparent ); mixMethods( methods['protected'], dfn, 'protected', iname, inparent ); // if this class inherits from another class that is *not* the base // class, recursively process its methods; otherwise, we will have // incompletely proxied the prototype chain var parent = methods['public'].___$$parent$$; if ( parent && ( parent.constructor !== ClassBuilder.ClassBase ) ) { mixinCls( parent.constructor, dfn, iname, true ); } } /** * Mix implemented types into destination object * * The provided destination object will ideally be the `implemented' array * of the destination class's meta object. * * @param {Class} cls source class * @param {Object} dest_meta destination object to copy into * * @return {undefined} */ function mixinImpl( cls, dest_meta ) { var impl = ClassBuilder.getMeta( cls ).implemented || [], i = impl.length; while ( i-- ) { // TODO: this could potentially result in duplicates dest_meta.push( impl[ i ] ); } } /** * Mix methods from SRC into DEST using proxies * * @param {Object} src visibility object to scavenge from * @param {Object} dest destination definition object * @param {string} vis visibility modifier * @param {string} iname proxy destination (trait instance) * * @return {undefined} */ function mixMethods( src, dest, vis, iname, inparent ) { for ( var f in src ) { if ( !( Object.hasOwnProperty.call( src, f ) ) ) { 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 if ( !( src[ f ] && src[ f ].___$$keywords$$ ) ) { continue; } var keywords = src[ f ].___$$keywords$$; // TODO: This is a kluge to handle ES3 fallbacks, which will cause // protected members to appear on the public prototype. A more // elegant solution is to automatically add the public keyword when // the class is built, so we can just check if keywords[vis] exists. if ( ( vis === 'public' ) && keywords[ 'protected' ] ) { continue; } vis = keywords[ 'protected' ] ? 'protected' : 'public'; // if abstract, then we are expected to provide the implementation; // otherwise, we proxy to the trait's implementation if ( keywords[ 'abstract' ] && !( keywords[ 'override' ] ) ) { // copy the abstract definition (N.B. this does not copy the // param names, since that is not [yet] important); the // visibility modifier is important to prevent de-escalation // errors on override dest[ vis + ' weak abstract ' + f ] = src[ f ].definition; } else if ( inparent && !keywords[ 'abstract' ] ) { continue; } else { var vk = keywords['virtual'], virt = vk ? 'virtual ' : '', ovr = ( keywords['override'] ) ? 'override ' : '', pname = ( vk ? '' : 'proxy ' ) + virt + ovr + vis + ' ' + f; // if we have already set up a proxy for a field of this name, // then multiple traits have defined the same concrete member if ( dest[ pname ] !== undefined ) { // TODO: between what traits? throw Error( "Trait member conflict: `" + f + "'" ); } // if non-virtual, a normal proxy should do if ( !( keywords[ 'virtual' ] ) ) { dest[ pname ] = iname; continue; } // proxy this method to what will be the encapsulated trait // object (note that we do not use the proxy keyword here // beacuse we are not proxying to a method of the same name) dest[ pname ] = ( function( f ) { var retf = function() { var pdest = this[ iname ]; // invoke the direct method on the trait instance; this // bypasses the virtual override check on the trait // method to ensure that it is invoked without // additional overhead or confusion var ret = pdest[ '__$$' + f ].apply( pdest, arguments ); // if the trait returns itself, return us instead return ( ret === pdest ) ? this : ret; }; retf.__length = src[ f ].__length; return retf; } )( f ); } } } /** * Add concrete trait class to a class instantion list * * This list---which will be created if it does not already exist---will be * used upon instantiation of the class consuming DFN to instantiate the * concrete trait classes. * * 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} base target supertype * * @return {string} private member into which C instance shall be stored */ function addTraitInst( T, dfn, tc, base ) { 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 tc.push( [ iname, T ] ); // we must also add the private field to the definition object to // support the object assignment indicated by TC dfn[ 'private ' + iname ] = null; // create internal trait ctor if not available if ( dfn.___$$tctor$$ === undefined ) { // TODO: let's check for inheritance or something to avoid this weak // 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, base ); } return iname; } /** * Trait initialization constructor * * May be used to initialize all traits mixed into the class that invokes * this function. All concrete trait classes are instantiated and their * 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. * * Note that the private symbol used to encapsulate class data must be * passed to this function to provide us access to implementation details * that we really shouldn't be messing around with. :) In particular, we * need access to the protected visibility object, and there is [currently] * no API for doing so. * * @param {Object} tc trait class list * @param {Class} base target supertype * @param {Symbol} privsym symbol used as key for encapsulated data * * @return {undefined} */ function tctor( tc, base, privsym ) { // instantiate all traits and assign the object to their // respective fields for ( var t in tc ) { var f = tc[ t ][ 0 ], T = tc[ t ][ 1 ], C = T.__ccls || ( T.__ccls = createConcrete( T.__acls ) ); // instantiate the trait, providing it with our protected visibility // object so that it has access to our public and protected members // (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( 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 this.__super && this.__super( privsym ); }; /** * Create trait constructor * * This binds the generic trait constructor to a reference to the provided * trait class list. * * @param {Object} tc trait class list * @param {Class} base target supertype * * @return {function()} trait constructor */ function createTctor( tc, base ) { return function( privsym ) { return tctor.call( this, tc, base, privsym ); }; } function _tctorApply() { this.___$$tctor$$.apply( this, arguments ); } module.exports = Trait;