From 6473cf35ae7553ae8f7db08d2cc19710eba353dc Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 19 Feb 2014 00:26:57 -0500 Subject: [PATCH] Began Scala-influenced linearization implementation More information on this implementation and the rationale behind it will appear in the manual. See future commits. (Note the TODOs; return values aren't quite right here, but that will be handled in the next commit.) --- lib/MethodWrappers.js | 3 -- lib/Trait.js | 66 ++++++++++++++++++++------- test/Trait/LinearizationTest.js | 80 +++++++++++++++++++++++++++++++++ test/Trait/VirtualTest.js | 17 ++++--- 4 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 test/Trait/LinearizationTest.js diff --git a/lib/MethodWrappers.js b/lib/MethodWrappers.js index c2479b9..086d2ad 100644 --- a/lib/MethodWrappers.js +++ b/lib/MethodWrappers.js @@ -123,9 +123,6 @@ exports.standard = { : retval; }; - // TODO: need a test for this; yes, we want to store a reference - ret.___$$proxy_to$$ = proxy_to; - // ensures that proxies can be used to provide concrete // implementations of abstract methods with param requirements (we // have no idea what we'll be proxying to at runtime, so we need to diff --git a/lib/Trait.js b/lib/Trait.js index a83f12f..7d1ed7e 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -213,23 +213,31 @@ function createVirtProxy( acls, dfn ) ? 'public' : 'protected'; + // this is the aforementioned proxy method; see the docblock for + // more information dfn[ vis + ' virtual override ' + f ] = ( function() { - // this is the aforementioned proxy method; see the docblock for - // more information return function() { var pmo = this.___$$pmo$$, - o = pmo[ f ], - op = o.___$$proxy_to$$; + o = pmo[ f ]; - // XXX: a better way to do this would be nice, since this - // does a substring check on every call; avoids infinite - // recursion from proxying to self - return ( o && !( op && op.substr( 0, 7 ) === '___$to$' ) ) - ? pmo[ f ].apply( pmo, arguments ) + // 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 ); - } + }; + } )( f ); + + // this guy bypasses the above virtual override check, which is + // necessary in certain cases to prevent infinte recursion + dfn[ vis + ' virtual __$$' + f ] = ( function( f ) + { + return function() + { + this.___$$parent$$[ f ].apply( this, arguments ); + }; } )( f ); } } @@ -271,7 +279,7 @@ function mixin( trait, dfn, tc ) * @param {Object} src visibility object to scavenge from * @param {Object} dest destination definition object * @param {string} vis visibility modifier - * @param {string} ianem proxy destination (trait instance) + * @param {string} iname proxy destination (trait instance) * * @return {undefined} */ @@ -307,8 +315,9 @@ function mixMethods( src, dest, vis, iname ) } else { - var virt = keywords['virtual'] ? 'weak virtual ' : '', - pname = virt + vis + ' proxy ' + f; + var vk = keywords['virtual'], + virt = vk ? 'weak virtual ' : '', + pname = ( vk ? '' : 'proxy ' ) + virt + 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 @@ -318,9 +327,36 @@ function mixMethods( src, dest, vis, iname ) throw Error( "Trait member conflict: `" + f + "'" ); } + // if non-virtual, a normal proxy should do + // TODO: test return value; see below + if ( !( keywords['virtual'] ) ) + { + dest[ pname ] = iname; + continue; + } + // proxy this method to what will be the encapsulated trait - // object - dest[ pname ] = iname; + // 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 ) + { + return 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 ); + + // TODO: test return value + // if the trait returns itself, return us instead + return ( ret === iname ) + ? this + : ret; + }; + } )( f ); } } } diff --git a/test/Trait/LinearizationTest.js b/test/Trait/LinearizationTest.js new file mode 100644 index 0000000..88d0728 --- /dev/null +++ b/test/Trait/LinearizationTest.js @@ -0,0 +1,80 @@ +/** + * Tests trait/class linearization + * + * Copyright (C) 2014 Mike Gerwitz + * + * 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 . + * + * GNU ease.js adopts Scala's concept of `linearization' with respect to + * resolving calls to supertypes; the tests that follow provide a detailed + * description of the concept, but readers may find it helpful to read + * through the ease.js manual or Scala documentation. + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'Trait' ); + this.Class = this.require( 'class' ); + }, + + + /** + * When a class mixes in a trait that defines some method M, and then + * overrides it as M', then this.__super within M' should refer to M. + * Note that this does not cause any conflicts with any class supertypes + * that may define a method of the same name as M, because M must have + * been an override, otherwise an error would have occurred. + */ + 'Class super call refers to mixin that is part of a class definition': + function() + { + var _self = this, + scalled = false; + + var T = this.Sut( + { + // after mixin, this should be the super method + 'virtual public foo': function() + { + scalled = true; + }, + } ); + + this.Class.use( T ).extend( + { + // overrides mixed-in foo + 'override public foo': function() + { + // should invoke T.foo + try + { + this.__super(); + } + catch ( e ) + { + _self.fail( false, true, + "Super invocation failure: " + e.message + ); + } + }, + } )().foo(); + + this.assertOk( scalled ); + }, +} ); + diff --git a/test/Trait/VirtualTest.js b/test/Trait/VirtualTest.js index df02742..82ac37f 100644 --- a/test/Trait/VirtualTest.js +++ b/test/Trait/VirtualTest.js @@ -17,6 +17,9 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * Note that tests for super calls are contained within LinearizationTest; + * these test cases simply ensure that overrides are actually taking place. */ require( 'common' ).testCase( @@ -34,23 +37,24 @@ require( 'common' ).testCase( */ 'Class inherits virtual trait method': function() { - var expected = 'foobar'; + var called = false; var T = this.Sut( { 'virtual foo': function() { - return expected; + called = true; } } ); var C = this.Class.use( T ).extend( {} ); // ensure that we are actually using the method - this.assertEqual( C().foo(), expected ); + C().foo(); + this.assertOk( called, "Virtual method not called" ); // if virtual, we should be able to override it - var expected2 = 'foobaz', + var called2 = false, C2; this.assertDoesNotThrow( function() @@ -59,12 +63,13 @@ require( 'common' ).testCase( { 'override foo': function() { - return expected2; + called2 = true; } } ); } ); - this.assertEqual( C2().foo(), expected2 ); + C2().foo(); + this.assertOk( called2, "Method not overridden" ); },