Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

translated membranes to direct proxies, added more unit tests

  • Loading branch information...
commit 0bc2fa0cae930d472b8d59f537487fc925280add 1 parent b24ff49
@tvcutsem authored
Showing with 1,080 additions and 260 deletions.
  1. +405 −0 examples/membrane.js
  2. +356 −260 notification/membrane.js
  3. +319 −0 test/membrane_test.js
View
405 examples/membrane.js
@@ -0,0 +1,405 @@
+// Copyright (C) 2013 Software Languages Lab, Vrije Universiteit Brussel
+// This code is dual-licensed under both the Apache License and the MPL
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/* Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is membranes using Direct Proxies
+ *
+ * The Initial Developer of the Original Code is
+ * Tom Van Cutsem, Vrije Universiteit Brussel.
+ * Portions created by the Initial Developer are Copyright (C) 2013
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ */
+
+// ----------------------------------------------------------------------------
+
+// requires Direct Proxies
+// load('reflect.js') before using
+
+(function(exports){
+ "use strict";
+
+ // == Auxiliaries ==
+
+ function assert(bool) {
+ if (!bool) throw new Error("assertion failed");
+ }
+
+ /**
+ * Overwrites `dst.name` with `src.name`.
+ * Either succeeds silently or fails noisily with a TypeError.
+ */
+ function copy(src, dst, name, wrap) {
+ var srcDesc = Reflect.getOwnPropertyDescriptor(src, name);
+ if (srcDesc === undefined) {
+ delete dst[name];
+ } else {
+
+ // simply calling wrappedSrcDesc = wrap(srcDesc) leads to infinite recursion,
+ // possible cause: internal algo's query srcDesc for its attributes, which are
+ // themselves new objects that need to be wrapped, which themselves have attributes,
+ // which need to be wrapped, etc.
+ // Hence, we manually wrap the source property descriptor:
+ var wrappedSrcDesc = Object.create(null);
+ wrappedSrcDesc.enumerable = srcDesc.enumerable;
+ wrappedSrcDesc.configurable = srcDesc.configurable;
+ if ('value' in srcDesc) {
+ wrappedSrcDesc.value = wrap(srcDesc.value);
+ wrappedSrcDesc.writable = srcDesc.writable;
+ } else {
+ wrappedSrcDesc.get = wrap(srcDesc.get);
+ wrappedSrcDesc.set = wrap(srcDesc.set);
+ }
+
+ // Case-analysis, assuming dstDesc = Object.getOwnPropertyDescriptor(dst, name);
+ // srcDesc is configurable, dstDesc is configurable or undefined
+ // => dstDesc is unconditionally overridden
+ // srcDesc is configurable, dstDesc is non-configurable
+ // => should not occur (dstDesc can only be non-configurable
+ // if srcDesc was previously non-configurable)
+ // srcDesc is non-configurable, dstDesc is configurable or undefined
+ // => dstDesc is unconditionally overridden
+ // srcDesc is non-configurable, dstDesc is non-configurable
+ // => Object.defineProperty should tolerate the update as long as all values
+ // are identical. OK since wrap preserves identity of wrappers.
+ Object.defineProperty(dst, name, wrappedSrcDesc);
+ }
+ }
+
+ /**
+ * Copy all "own" properties from 'src' to 'dst'. Properties are wrapped
+ * using 'wrap' before being defined on the 'dst' object.
+ */
+ function copyAll(src, dst, wrap) {
+ Object.getOwnPropertyNames(src).forEach(function (name) {
+ copy(src, dst, name, wrap);
+ });
+ }
+
+ // == The interesting stuff ==
+
+ /**
+ * A revocable membrane abstraction expressed using Direct Proxies.
+ *
+ * @author tvcutsem
+ *
+ * For a general introduction to membranes, see:
+ * http://soft.vub.ac.be/~tvcutsem/invokedynamic/js-membranes
+ *
+ * Usage:
+ * var membrane = makeMembrane(wetObject)
+ * var dryObject = membrane.target;
+ * var dryX = dryObject.x; // accessed objects are recursively wrapped
+ * membrane.revoke(); // touching any dry value after this point throws
+ *
+ * Note: once the membrane is revoked, access to wrapped objects is no longer
+ * possible, although GC can't detect the revocation since we're not using
+ * revocable proxies.
+ */
+ function makeMembrane(initWetTarget) {
+
+ /****************************************************************************
+ Design notes:
+
+ A membrane separates two "realms" (object graphs): a "wet" realm and a "dry"
+ realm. Every non-primitive value that crosses the membrane gets wrapped in a
+ proxy. The membrane uses two WeakMaps to identify and cache previously
+ created wrappers, so that for any "wet" value on the inside of the membrane,
+ only a single "dry" wrapper gets created. This preserves identity across the
+ membrane.
+
+ Values can flow in two directions through the membrane:
+
+ * dry -> wet: typically via argument-passing
+ * wet -> dry: typically via return values or thrown exceptions
+
+ The membrane distinguishes between these two directions by allocating a
+ different wrapper, and maintaining a separate WeakMap cache, per direction.
+
+ To be able to properly represent non-configurability and non-extensibility
+ invariants, the membrane's Proxy wrappers make use of a dummy, "shadow"
+ target. The "shadow", while being encapsulated inside the proxy, actually
+ still counts as being on the opposite side of the membrane.
+
+ Dry (outside) | Wet (inside)
+ |
+ DryObject --> DryToWetProxy --> DryShadowTarget
+ \--> WetRealTarget
+ |
+ WetShadowTarget <-- WetToDryProxy <-- WetObject
+ DryRealTarget <--/
+ |
+ |
+
+ In the above drawing, the DryToWetProxy is a proxy for the WetRealTarget,
+ but the "target" of the Proxy abstraction is DryShadowTarget.
+
+ Before the DryToWetProxy forwards the intercepted operation to
+ WetRealTarget, the state of the DryShadowTarget is "synchronized" with
+ that of the WetRealTarget. When the proxy next forwards the
+ intercepted operation to the DryShadowTarget, the shadow will return the
+ appropriate result.
+
+ If the intercepted operation is supposed to side-effect the target, then
+ after the operation was forwarded, the proxy propagates the update from the
+ DryShadowTarget to the WetRealTarget.
+
+ Throughout the remainder of this code, we use the naming prefix "wet" or "dry"
+ consistently as a sort of type annotation to identify the provenance of the
+ named value.
+
+ ****************************************************************************/
+
+ var revoked = false; // is the membrane revoked yet?
+
+ // on first read, it may help to skip down below to see how this function
+ // is used
+ var dryToWetMaker = function(dryToWetCache, wetToDryCache, dryToWetRef, wetToDryRef) {
+
+ return function(dryTarget) {
+
+ if (Object(dryTarget) !== dryTarget) {
+ return dryTarget; // primitives are passed through unwrapped
+ }
+
+ var wetToDryWrapper = dryToWetCache.get(dryTarget);
+ if (wetToDryWrapper) {
+ return wetToDryWrapper; // already got a wrapper
+ }
+
+ var dryToWet = dryToWetRef.val;
+ var wetToDry = wetToDryRef.val;
+
+ // need to make a new wrapper
+
+ var wetShadowTarget;
+
+ if (typeof dryTarget === "function") {
+ wetShadowTarget = function() {
+ if (revoked) throw new Error("revoked");
+ var wetArgs = Array.prototype.slice.call(arguments);
+ var wetThis = this;
+ var dryArgs = wetArgs.map(wetToDry);
+ var dryThis = wetToDry(wetThis);
+ try {
+ var dryResult = Reflect.apply(dryTarget, dryThis, dryArgs);
+ var wetResult = dryToWet(dryResult);
+ return wetResult;
+ } catch (dryException) {
+ throw dryToWet(dryException);
+ }
+ };
+ } else {
+ var dryProto = Object.getPrototypeOf(dryTarget);
+ wetShadowTarget = Object.create(dryToWet(dryProto));
+ }
+
+ wetToDryWrapper = new Proxy(wetShadowTarget, {
+
+ getOwnPropertyDescriptor: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return Reflect.getOwnPropertyDescriptor(wetShadowTarget, name);
+ },
+
+ getOwnPropertyNames: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ return Reflect.getOwnPropertyNames(wetShadowTarget);
+ },
+
+ getPrototypeOf: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ // if non-standard __proto__ is available, use it to synchronize the prototype
+ // as it may have changed since wetShadowTarget was first created
+ if (({}).__proto__ !== undefined) {
+ var dryProto = Object.getPrototypeOf(dryTarget);
+ try {
+ wetShadowTarget.__proto__ = dryToWet(dryProto);
+ } catch (e) {
+ // fails on FF with TypeError:
+ // can't redefine non-configurable property '__proto__'
+ }
+ }
+ return Reflect.getPrototypeOf(wetShadowTarget);
+ },
+
+ defineProperty: function(wetShadowTarget, name, wetDesc) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ var success = Reflect.defineProperty(wetShadowTarget, name, wetDesc);
+ if (success) {
+ copy(wetShadowTarget, dryTarget, name, wetToDry);
+ }
+ return success;
+ },
+
+ deleteProperty: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ var success = Reflect.deleteProperty(wetShadowTarget, name);
+ if (success) {
+ copy(wetShadowTarget, dryTarget, name, wetToDry);
+ }
+ return success;
+ },
+
+ preventExtensions: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ Object.preventExtensions(dryTarget);
+ return Reflect.preventExtensions(wetShadowTarget);
+ },
+
+ isExtensible: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ if (!Object.isExtensible(dryTarget)) {
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ Object.preventExtensions(wetShadowTarget);
+ }
+ return Reflect.isExtensible(wetShadowTarget);
+ },
+
+ // FIXME: skipped onFreeze, onSeal, onIsFrozen, onIsSealed
+
+ has: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return Reflect.has(wetShadowTarget, name);
+ },
+
+ hasOwn: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return Reflect.hasOwn(wetShadowTarget, name);
+ },
+
+ get: function(wetShadowTarget, name, wetReceiver) {
+ if (revoked) throw new Error("revoked");
+ // How does wetReceiver get wrapped?
+ // - assume dryTarget.name is an accessor
+ // - then the following line will define wetShadowTarget.name
+ // as a wrapped accessor
+ // - the wrapped accessor's "get" function will be a function proxy...
+ // - ... which does the wrapping when it gets applied
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return Reflect.get(wetShadowTarget, name, wetReceiver);
+ },
+
+ set: function(wetShadowTarget, name, val, wetReceiver) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ var success = Reflect.set(wetShadowTarget, name, val, wetReceiver);
+ if (success) {
+ copy(wetShadowTarget, dryTarget, name, wetToDry);
+ }
+ return success;
+ },
+
+ enumerate: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ return Reflect.enumerate(wetShadowTarget);
+ },
+
+ keys: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ return Reflect.keys(wetShadowTarget);
+ },
+
+ apply: function(wetShadowTarget, wetThisArg, wetArgs) {
+ if (revoked) throw new Error("revoked");
+ return Reflect.apply(wetShadowTarget, wetThisArg, wetArgs);
+ },
+
+ construct: function(wetShadowTarget, wetArgs) {
+ if (revoked) throw new Error("revoked");
+ return Reflect.construct(wetShadowTarget, wetArgs);
+ }
+
+ }); // end wetToDryWrapper = new Proxy(...)
+
+ dryToWetCache.set(dryTarget, wetToDryWrapper);
+ wetToDryCache.set(wetToDryWrapper, dryTarget);
+ return wetToDryWrapper;
+ }; // end function(dryTarget)
+
+ }; // end dryToWetMaker
+
+ var dryToWetCache = new WeakMap();
+ var wetToDryCache = new WeakMap();
+
+ // dryToWet needs wetToDry and vice-versa -> cyclic dependency
+ // this dependency is broken by introducing explicit "reference" objects
+ // whose 'val' property acts as an explicit pointer.
+ var dryToWetRef = {val:null};
+ var wetToDryRef = {val:null};
+
+ dryToWetRef.val =
+ dryToWetMaker(dryToWetCache, wetToDryCache, dryToWetRef, wetToDryRef);
+
+ // note the reversed order of wetToDry and dryToWet:
+
+ wetToDryRef.val =
+ dryToWetMaker(wetToDryCache, dryToWetCache, wetToDryRef, dryToWetRef);
+
+ return {
+ target: wetToDryRef.val(initWetTarget),
+ revoke: function() {
+ dryToWetCache = null;
+ wetToDryCache = null;
+ revoked = true;
+ }
+ };
+ }; // end makeMembrane
+
+ // trivial unit test, see ../test/membrane_test.js for more
+ function testMembrane() {
+ load('../reflect.js');
+ var wetTarget = {x: {y:2}};
+ var membrane = makeMembrane(wetTarget);
+ var dryProxy = membrane.target;
+
+ var dryFoo = dryProxy.x;
+ assert(dryFoo.y === 2);
+ membrane.revoke();
+ try {
+ dryFoo.y; // TypeError: revoked
+ assert(false);
+ } catch (e) {
+ assert(/revoked/.test(e.toString()));
+ }
+ print('ok');
+ }
+
+ // testMembrane();
+
+ exports.makeMembrane = makeMembrane;
+
+}(typeof exports !== "undefined" ? exports : this));
View
616 notification/membrane.js
@@ -25,7 +25,7 @@
* for the specific language governing rights and limitations under the
* License.
*
- * The Original Code is an example of Notification Proxies
+ * The Original Code is membranes using Notification Proxies
*
* The Initial Developer of the Original Code is
* Tom Van Cutsem, Vrije Universiteit Brussel.
@@ -38,281 +38,377 @@
// ----------------------------------------------------------------------------
-"use strict";
-load('notify-reflect.js');
-
-// == Auxiliaries ==
+// requires Notification Proxies
+// load('notify-reflect.js') before using
+
+(function(exports){
+ "use strict";
+
+ // == Auxiliaries ==
-function assert(bool) {
- if (!bool) throw new Error("assertion failed");
-}
+ function assert(bool) {
+ if (!bool) throw new Error("assertion failed");
+ }
-/**
- * Overwrites `dst.name` with `src.name`.
- * Either succeeds silently or fails noisily with a TypeError.
- */
-function copy(src, dst, name, wrap) {
- var srcDesc = Reflect.getOwnPropertyDescriptor(src, name);
- if (srcDesc === undefined) {
- delete dst[name];
- } else {
-
- // simply calling wrappedSrcDesc = wrap(srcDesc) leads to infinite recursion,
- // possible cause: internal algo's query srcDesc for its attributes, which are
- // themselves new objects that need to be wrapped, which themselves have attributes,
- // which need to be wrapped, etc.
- // Hence, we manually wrap the source property descriptor:
- var wrappedSrcDesc = Object.create(null);
- wrappedSrcDesc.enumerable = srcDesc.enumerable;
- wrappedSrcDesc.configurable = srcDesc.configurable;
- if ('value' in srcDesc) {
- wrappedSrcDesc.value = wrap(srcDesc.value);
- wrappedSrcDesc.writable = srcDesc.writable;
+ /**
+ * Overwrites `dst.name` with `src.name`.
+ * Either succeeds silently or fails noisily with a TypeError.
+ */
+ function copy(src, dst, name, wrap) {
+ var srcDesc = Reflect.getOwnPropertyDescriptor(src, name);
+ if (srcDesc === undefined) {
+ delete dst[name];
} else {
- wrappedSrcDesc.get = wrap(srcDesc.get);
- wrappedSrcDesc.set = wrap(srcDesc.set);
+
+ // simply calling wrappedSrcDesc = wrap(srcDesc) leads to infinite recursion,
+ // possible cause: internal algo's query srcDesc for its attributes, which are
+ // themselves new objects that need to be wrapped, which themselves have attributes,
+ // which need to be wrapped, etc.
+ // Hence, we manually wrap the source property descriptor:
+ var wrappedSrcDesc = Object.create(null);
+ wrappedSrcDesc.enumerable = srcDesc.enumerable;
+ wrappedSrcDesc.configurable = srcDesc.configurable;
+ if ('value' in srcDesc) {
+ wrappedSrcDesc.value = wrap(srcDesc.value);
+ wrappedSrcDesc.writable = srcDesc.writable;
+ } else {
+ wrappedSrcDesc.get = wrap(srcDesc.get);
+ wrappedSrcDesc.set = wrap(srcDesc.set);
+ }
+
+ // Case-analysis, assuming dstDesc = Object.getOwnPropertyDescriptor(dst, name);
+ // srcDesc is configurable, dstDesc is configurable or undefined
+ // => dstDesc is unconditionally overridden
+ // srcDesc is configurable, dstDesc is non-configurable
+ // => should not occur (dstDesc can only be non-configurable
+ // if srcDesc was previously non-configurable)
+ // srcDesc is non-configurable, dstDesc is configurable or undefined
+ // => dstDesc is unconditionally overridden
+ // srcDesc is non-configurable, dstDesc is non-configurable
+ // => Object.defineProperty should tolerate the update as long as all values
+ // are identical. OK since wrap preserves identity of wrappers.
+ Object.defineProperty(dst, name, wrappedSrcDesc);
}
+ }
- // Case-analysis, assuming dstDesc = Object.getOwnPropertyDescriptor(dst, name);
- // srcDesc is configurable, dstDesc is configurable or undefined
- // => dstDesc is unconditionally overridden
- // srcDesc is configurable, dstDesc is non-configurable
- // => should not occur (dstDesc can only be non-configurable
- // if srcDesc was previously non-configurable)
- // srcDesc is non-configurable, dstDesc is configurable or undefined
- // => dstDesc is unconditionally overridden
- // srcDesc is non-configurable, dstDesc is non-configurable
- // => Object.defineProperty should tolerate the update as long as all values
- // are identical. OK since wrap preserves identity of wrappers.
- Object.defineProperty(dst, name, wrappedSrcDesc);
+ /**
+ * Copy all "own" properties from 'src' to 'dst'. Properties are wrapped
+ * using 'wrap' before being defined on the 'dst' object.
+ */
+ function copyAll(src, dst, wrap) {
+ Object.getOwnPropertyNames(src).forEach(function (name) {
+ copy(src, dst, name, wrap);
+ });
}
-}
-/**
- * Copy all "own" properties from 'src' to 'dst'. Properties are wrapped
- * using 'wrap' before being defined on the 'dst' object.
- */
-function copyAll(src, dst, wrap) {
- Object.getOwnPropertyNames(src).forEach(function (name) {
- copy(src, dst, name, wrap);
- });
-}
-
-/**
- * A membrane abstraction expressed using Notification Proxies.
- *
- * @author tvcutsem
- *
- * For a general introduction to membranes, see:
- * http://soft.vub.ac.be/~tvcutsem/invokedynamic/js-membranes
- *
- * Usage:
- * var membrane = makeMembrane(wetObject)
- * var dryObject = membrane.target;
- * var dryX = dryObject.x; // accessed objects are recursively wrapped
- * membrane.revoke(); // touching any dry value after this point throws
- */
-function makeMembrane(initWetTarget) {
-
- var revoked = false;
-
- // Dry ---> DryToWetProxy ---> DryShadowTarget
- // ---> WetRealTarget
-
- // Wet ---> WetToDryProxy ---> WetShadowTarget
- // ---> DryRealTarget
-
- var dryToWetMaker = function(scope, dryToWetName, wetToDryName) {
-
- var dryToWetCache = new WeakMap();
-
- return function(dryTarget) {
+ // == The interesting stuff ==
+
+ /**
+ * A revocable membrane abstraction expressed using Notification Proxies.
+ *
+ * @author tvcutsem
+ *
+ * For a general introduction to membranes, see:
+ * http://soft.vub.ac.be/~tvcutsem/invokedynamic/js-membranes
+ *
+ * Usage:
+ * var membrane = makeMembrane(wetObject)
+ * var dryObject = membrane.target;
+ * var dryX = dryObject.x; // accessed objects are recursively wrapped
+ * membrane.revoke(); // touching any dry value after this point throws
+ *
+ * Note: once the membrane is revoked, access to wrapped objects is no longer
+ * possible, although GC can't detect the revocation since we're not using
+ * revocable proxies.
+ */
+ function makeMembrane(initWetTarget) {
+
+ /****************************************************************************
+ Design notes:
+
+ A membrane separates two "realms" (object graphs): a "wet" realm and a "dry"
+ realm. Every non-primitive value that crosses the membrane gets wrapped in a
+ proxy. The membrane uses two WeakMaps to identify and cache previously
+ created wrappers, so that for any "wet" value on the inside of the membrane,
+ only a single "dry" wrapper gets created. This preserves identity across the
+ membrane.
+
+ Values can flow in two directions through the membrane:
+
+ * dry -> wet: typically via argument-passing
+ * wet -> dry: typically via return values or thrown exceptions
+
+ The membrane distinguishes between these two directions by allocating a
+ different wrapper, and maintaining a separate WeakMap cache, per direction.
+
+ To be able to properly represent non-configurability and non-extensibility
+ invariants, the membrane's Proxy wrappers make use of a dummy, "shadow"
+ target. The "shadow", while being encapsulated inside the proxy, actually
+ still counts as being on the opposite side of the membrane.
+
+ Dry (outside) | Wet (inside)
+ |
+ DryObject --> DryToWetProxy --> DryShadowTarget
+ \--> WetRealTarget
+ |
+ WetShadowTarget <-- WetToDryProxy <-- WetObject
+ DryRealTarget <--/
+ |
+ |
+
+ In the above drawing, the DryToWetProxy is a proxy for the WetRealTarget,
+ but the "target" of the Proxy abstraction is DryShadowTarget.
+
+ In every pre-trap, the state of the DryShadowTarget is "synchronized" with
+ that of the WetRealTarget. When the notification proxy next forwards the
+ intercepted operation to the DryShadowTarget, the shadow will return the
+ appropriate result.
+
+ If the intercepted operation is supposed to side-effect the target, then
+ a post-trap is registered that propagates the update from the
+ DryShadowTarget to the WetRealTarget.
+
+ If the intercepted operation does not side-effect the target, then no
+ post-trap is required and the pre-trap simply returns undefined.
+
+ Throughout the remainder of this code, we use the naming prefix "wet" or "dry"
+ consistently as a sort of type annotation to identify the provenance of the
+ named value.
+
+ ****************************************************************************/
+
+ var revoked = false; // is the membrane revoked yet?
+
+ // on first read, it may help to skip down below to see how this function
+ // is used
+ var dryToWetMaker = function(dryToWetCache, wetToDryCache, dryToWetRef, wetToDryRef) {
+
+ return function(dryTarget) {
+
+ if (Object(dryTarget) !== dryTarget) {
+ return dryTarget; // primitives are passed through unwrapped
+ }
- if (Object(dryTarget) !== dryTarget) {
- return dryTarget; // primitives are passed through
- }
-
- var wetToDryWrapper = dryToWetCache.get(dryTarget);
- if (wetToDryWrapper) {
- return wetToDryWrapper; // already got a wrapper
- }
+ var wetToDryWrapper = dryToWetCache.get(dryTarget);
+ if (wetToDryWrapper) {
+ return wetToDryWrapper; // already got a wrapper
+ }
- var dryToWet = scope[dryToWetName];
- var wetToDry = scope[wetToDryName];
-
- // need to make a new wrapper
- var wetShadowTarget;
-
- // DEBUG: print('wrapping ' + dryTarget.toSource());
-
- if (typeof dryTarget === "function") {
- wetShadowTarget = function() {
- var wetArgs = Array.prototype.slice.call(arguments);
- var wetThis = this;
- var dryArgs = wetArgs.map(wetToDry);
- var dryThis = dryToWet(wetThis);
- var dryResult = Reflect.apply(dryTarget, dryThis, dryArgs);
- var wetResult = wetToDry(dryResult);
- return wetResult;
- };
- } else {
- var dryProto = Object.getPrototypeOf(dryTarget);
- wetShadowTarget = Object.create(dryToWet(dryProto));
- }
-
- wetToDryWrapper = new Proxy(wetShadowTarget, {
-
- onGetOwnPropertyDescriptor: function(wetShadowTarget, name) {
- if (revoked) throw new Error("revoked");
- copy(dryTarget, wetShadowTarget, name, dryToWet);
- return undefined;
- },
-
- onGetOwnPropertyNames: function(wetShadowTarget) {
- if (revoked) throw new Error("revoked");
- copyAll(dryTarget, wetShadowTarget, dryToWet);
- return undefined;
- },
-
- onGetPrototypeOf: function(wetShadowTarget) {
- if (revoked) throw new Error("revoked");
- // if non-standard __proto__ is available, use it to synchronize the prototype
- // as it may have changed since wetShadowTarget was first created
- if (({}).__proto__ !== undefined) {
- var dryProto = Object.getPrototypeOf(dryTarget);
- wetShadowTarget.__proto__ = dryToWet(dryProto);
- }
- return undefined;
- },
-
- onDefineProperty: function(wetShadowTarget, name, wetDesc) {
- if (revoked) throw new Error("revoked");
- copy(dryTarget, wetShadowTarget, name, dryToWet);
- return function(wetShadowTarget, name, wetDesc, success) {
- if (success) {
- copy(wetShadowTarget, dryTarget, name, wetToDry);
+ var dryToWet = dryToWetRef.val;
+ var wetToDry = wetToDryRef.val;
+
+ // need to make a new wrapper
+
+ var wetShadowTarget;
+
+ if (typeof dryTarget === "function") {
+ wetShadowTarget = function() {
+ if (revoked) throw new Error("revoked");
+ var wetArgs = Array.prototype.slice.call(arguments);
+ var wetThis = this;
+ var dryArgs = wetArgs.map(wetToDry);
+ var dryThis = wetToDry(wetThis);
+ try {
+ var dryResult = Reflect.apply(dryTarget, dryThis, dryArgs);
+ var wetResult = dryToWet(dryResult);
+ return wetResult;
+ } catch (dryException) {
+ throw dryToWet(dryException);
}
};
- },
-
- onDeleteProperty: function(wetShadowTarget, name) {
- if (revoked) throw new Error("revoked");
- copy(dryTarget, wetShadowTarget, name, dryToWet);
- return function(wetShadowTarget, name, success) {
- if (success) {
- copy(wetShadowTarget, dryTarget, name, wetToDry);
- }
- };
- },
-
- onPreventExtensions: function(wetShadowTarget) {
- if (revoked) throw new Error("revoked");
- copyAll(dryTarget, wetShadowTarget, dryToWet);
- Object.preventExtensions(dryTarget);
- return function(wetShadowTarget, success) {
- assert(success === true)
- };
- },
+ } else {
+ var dryProto = Object.getPrototypeOf(dryTarget);
+ wetShadowTarget = Object.create(dryToWet(dryProto));
+ }
- onIsExtensible: function(wetShadowTarget) {
- if (revoked) throw new Error("revoked");
- if (!Object.isExtensible(dryTarget)) {
+ wetToDryWrapper = new Proxy(wetShadowTarget, {
+
+ onGetOwnPropertyDescriptor: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return undefined;
+ },
+
+ onGetOwnPropertyNames: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
copyAll(dryTarget, wetShadowTarget, dryToWet);
- Object.preventExtensions(wetShadowTarget);
- }
- return undefined;
- },
-
- onHas: function(wetShadowTarget, name) {
- if (revoked) throw new Error("revoked");
- copy(dryTarget, wetShadowTarget, name, dryToWet);
- return undefined;
- },
-
- onHasOwn: function(wetShadowTarget, name) {
- if (revoked) throw new Error("revoked");
- copy(dryTarget, wetShadowTarget, name, dryToWet);
- return undefined;
- },
-
- onGet: function(wetShadowTarget, name, wetReceiver) {
- if (revoked) throw new Error("revoked");
- // How does wetReceiver get wrapped?
- // - assume dryTarget.name is an accessor
- // - the following line will define wetShadowTarget.name as a wrapped accessor
- // - the wrapped accessor's "get" function will be a function proxy...
- // - ... which does the wrapping when it gets applied
- copy(dryTarget, wetShadowTarget, name, dryToWet);
- return undefined;
- // post-trap: function(wetShadowTarget, name, wetReceiver, wetResult) {}
- // note: return value is already 'wet', as it comes from
- // accessing a property on the wetShadowTarget
- },
-
- onSet: function(wetShadowTarget, name, val, wetReceiver) {
- if (revoked) throw new Error("revoked");
- copy(dryTarget, wetShadowTarget, name, dryToWet);
- return function(wetShadowTarget, name, wetReceiver, success) {
- if (success) {
- copy(wetShadowTarget, dryTarget, name, wetToDry);
+ return undefined;
+ },
+
+ onGetPrototypeOf: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ // if non-standard __proto__ is available, use it to synchronize the prototype
+ // as it may have changed since wetShadowTarget was first created
+ if (({}).__proto__ !== undefined) {
+ var dryProto = Object.getPrototypeOf(dryTarget);
+ try {
+ wetShadowTarget.__proto__ = dryToWet(dryProto);
+ } catch (e) {
+ // fails on FF with TypeError:
+ // can't redefine non-configurable property '__proto__'
+ }
}
+ return undefined;
+ },
+
+ onDefineProperty: function(wetShadowTarget, name, wetDesc) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return function(wetShadowTarget, name, wetDesc, success) {
+ if (success) {
+ copy(wetShadowTarget, dryTarget, name, wetToDry);
+ }
+ };
+ },
+
+ onDeleteProperty: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return function(wetShadowTarget, name, success) {
+ if (success) {
+ copy(wetShadowTarget, dryTarget, name, wetToDry);
+ }
+ };
+ },
+
+ onPreventExtensions: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ Object.preventExtensions(dryTarget);
+ return function(wetShadowTarget, success) {
+ // preventExtensions shouldn't fail on the shadow since
+ // it's a normal, non-proxy object
+ assert(success);
+ };
+ },
+
+ onIsExtensible: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ if (!Object.isExtensible(dryTarget)) {
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ Object.preventExtensions(wetShadowTarget);
+ }
+ return undefined;
+ },
+
+ // FIXME: skipped onFreeze, onSeal, onIsFrozen, onIsSealed
+
+ onHas: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return undefined;
+ },
+
+ onHasOwn: function(wetShadowTarget, name) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return undefined;
+ },
+
+ onGet: function(wetShadowTarget, name, wetReceiver) {
+ if (revoked) throw new Error("revoked");
+ // How does wetReceiver get wrapped?
+ // - assume dryTarget.name is an accessor
+ // - then the following line will define wetShadowTarget.name
+ // as a wrapped accessor
+ // - the wrapped accessor's "get" function will be a function proxy...
+ // - ... which does the wrapping when it gets applied
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return undefined;
+ // post-trap: function(wetShadowTarget, name, wetReceiver, wetResult) {}
+ // note: return value is already 'wet', as it comes from
+ // accessing a property on the wetShadowTarget
+ },
+
+ onSet: function(wetShadowTarget, name, val, wetReceiver) {
+ if (revoked) throw new Error("revoked");
+ copy(dryTarget, wetShadowTarget, name, dryToWet);
+ return function(wetShadowTarget, name, val, wetReceiver, success) {
+ if (success) {
+ copy(wetShadowTarget, dryTarget, name, wetToDry);
+ }
+ }
+ },
+
+ onEnumerate: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ return undefined;
+ },
+
+ onKeys: function(wetShadowTarget) {
+ if (revoked) throw new Error("revoked");
+ copyAll(dryTarget, wetShadowTarget, dryToWet);
+ return undefined;
+ },
+
+ onApply: function(wetShadowTarget, wetThisArg, wetArgs) {
+ if (revoked) throw new Error("revoked");
+ return undefined;
+ },
+
+ onConstruct: function(wetShadowTarget, wetArgs) {
+ if (revoked) throw new Error("revoked");
+ return undefined;
}
- },
-
- onEnumerate: function(wetShadowTarget) {
- if (revoked) throw new Error("revoked");
- copyAll(dryTarget, wetShadowTarget, dryToWet);
- return undefined;
- },
-
- onKeys: function(wetShadowTarget) {
- if (revoked) throw new Error("revoked");
- copyAll(dryTarget, wetShadowTarget, dryToWet);
- return undefined;
- },
-
- onApply: function(wetShadowTarget, wetThisArg, wetArgs) {
- if (revoked) throw new Error("revoked");
- return undefined;
- },
-
- onConstruct: function(wetShadowTarget, wetArgs) {
- if (revoked) throw new Error("revoked");
- return undefined;
- }
- }); // end wetToDryWrapper = new Proxy(...)
+ }); // end wetToDryWrapper = new Proxy(...)
+
+ dryToWetCache.set(dryTarget, wetToDryWrapper);
+ wetToDryCache.set(wetToDryWrapper, dryTarget);
+ return wetToDryWrapper;
+ }; // end function(dryTarget)
+
+ }; // end dryToWetMaker
+
+ var dryToWetCache = new WeakMap();
+ var wetToDryCache = new WeakMap();
+
+ // dryToWet needs wetToDry and vice-versa -> cyclic dependency
+ // this dependency is broken by introducing explicit "reference" objects
+ // whose 'val' property acts as an explicit pointer.
+ var dryToWetRef = {val:null};
+ var wetToDryRef = {val:null};
+
+ dryToWetRef.val =
+ dryToWetMaker(dryToWetCache, wetToDryCache, dryToWetRef, wetToDryRef);
+
+ // note the reversed order of wetToDry and dryToWet:
+
+ wetToDryRef.val =
+ dryToWetMaker(wetToDryCache, dryToWetCache, wetToDryRef, dryToWetRef);
+
+ return {
+ target: wetToDryRef.val(initWetTarget),
+ revoke: function() {
+ dryToWetCache = null;
+ wetToDryCache = null;
+ revoked = true;
+ }
+ };
+ }; // end makeMembrane
+
+ // trivial unit test, see ../test/membrane_test.js for more
+ function testMembrane() {
+ load('notify-reflect.js');
+ var wetTarget = {x: {y:2}};
+ var membrane = makeMembrane(wetTarget);
+ var dryProxy = membrane.target;
+
+ var dryFoo = dryProxy.x;
+ assert(dryFoo.y === 2);
+ membrane.revoke();
+ try {
+ dryFoo.y; // TypeError: revoked
+ assert(false);
+ } catch (e) {
+ assert(/revoked/.test(e.toString()));
+ }
+ print('ok');
+ }
- dryToWetCache.set(dryTarget, wetToDryWrapper);
- return wetToDryWrapper;
- }; // end function(dryTarget)
- }; // end dryToWetMaker
+ // testMembrane();
- // dryToWet needs wetToDry and vice-versa -> cyclic dependency
- // this dependency is broken by introducing an explicit scope object
- var scope = {};
- scope.dryToWet = dryToWetMaker(scope, 'dryToWet', 'wetToDry');
- scope.wetToDry = dryToWetMaker(scope, 'wetToDry', 'dryToWet');
+ exports.makeMembrane = makeMembrane;
- return {
- target: scope.wetToDry(initWetTarget),
- revoke: function() { revoked = true; }
- };
-}; // end makeMembrane
-
-// simple unit test
-
-var wetTarget = {x: {y:2}};
-var membrane = makeMembrane(wetTarget);
-var dryProxy = membrane.target;
-
-var dryFoo = dryProxy.x;
-print(dryFoo.y); // 2
-membrane.revoke();
-print(dryFoo.y); // TypeError: revoked
-
-// TODOs:
-// - test dry->wet mapping by passing a wet object
-// (or better yet: run direct proxy simple_membrane.js tests with this implementation)
-// - rename to membrane.js
-// - use this impl. to derive a full impl for direct proxies
+}(typeof exports !== "undefined" ? exports : this));
View
319 test/membrane_test.js
@@ -0,0 +1,319 @@
+// Copyright (C) 2013 Software Languages Lab, Vrije Universiteit Brussel
+// This code is dual-licensed under both the Apache License and the MPL
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/* Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is unit tests for membranes.
+ *
+ * The Initial Developer of the Original Code is
+ * Tom Van Cutsem, Vrije Universiteit Brussel.
+ * Portions created by the Initial Developer are Copyright (C) 2013
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ */
+
+// ----------------------------------------------------------------------------
+
+// test suite works both with membranes implemented using Direct Proxies as
+// well as Notification Proxies
+// for Direct Proxies, load '../examples/membrane.js'
+//load('../reflect.js');
+//load('../examples/membrane.js');
+
+// for Notification Proxies, load '../notification/membrane.js'
+load('../notification/notify-reflect.js');
+load('../notification/membrane.js');
+
+(function(){
+ "use strict";
+
+ function assert(b, reason) {
+ if (!b) throw new Error('assertion failed: '+reason);
+ }
+ function assertThrows(reason, msg, f) {
+ try {
+ f();
+ throw new Error('assertThrows: no exception raised: '+reason);
+ } catch(e) {
+ if (!msg.test(e.message)) {
+ throw e;
+ }
+ }
+ }
+
+ var TESTS = Object.create(null); // unit test functions stored in here
+
+ // each unit test is a function(membraneMaker)
+ // where membraneMaker is a function to construct an initial membrane
+
+ // test simple transitive wrapping
+ // test whether primitives make it through unwrapped
+ TESTS.testTransitiveWrapping = function(makeMembrane) {
+ // membrane works for configurable properties
+ var wetA = {x:1};
+ var wetB = {y:wetA};
+ var membrane = makeMembrane(wetB);
+ var dryB = membrane.target;
+ var dryA = dryB.y;
+ assert(wetA !== dryA, 'wetA !== dryA');
+ assert(wetB !== dryB, 'wetB !== dryB');
+ assert(wetA.x === 1, 'wetA.x === 1');
+ assert(dryA.x === 1, 'dryA.x === 1');
+ membrane.revoke();
+ assert(wetA.x === 1, 'wetA.x === 1 after revoke');
+ assert(wetB.y === wetA, 'wetB.y === wetA after revoke');
+ assertThrows('dryA.x', /revoked/, function() { dryA.x });
+ assertThrows('dryB.y', /revoked/, function() { dryB.y });
+ };
+
+ // test whether functions are wrapped
+ TESTS.testFunctionWrapping = function(makeMembrane) {
+ var wetA = function(x) { return x; };
+ var membrane = makeMembrane(wetA);
+ var dryA = membrane.target;
+
+ assert(wetA !== dryA, 'wetA !== dryA');
+ assert(wetA(1) === 1, 'wetA(1) === 1');
+ assert(dryA(1) === 1, 'dryA(1) === 1');
+
+ membrane.revoke();
+ assert(wetA(1) === 1, 'wetA(1) === 1 after revoke');
+ assertThrows('dryA(1) after revoke', /revoked/, function() { dryA(1) });
+ };
+
+ // test whether values returned from wrapped methods are wrapped
+ TESTS.testReturnWrapping = function(makeMembrane) {
+ var wetA = { x: 42 };
+ var wetB = {
+ m: function() { return wetA; }
+ };
+ var membrane = makeMembrane(wetB);
+ assert(wetA.x === 42, 'wetA.x === 42');
+ assert(wetB.m().x === 42, 'wetB.m().x === 42');
+
+ var dryB = membrane.target;
+ var dryA = dryB.m();
+
+ assert(wetA !== dryA, 'wetA !== dryA');
+ assert(wetB !== dryB, 'wetB !== dryB');
+
+ assert(dryA.x === 42, 'dryA.x === 42');
+
+ membrane.revoke();
+
+ assertThrows('dryA.x', /revoked/, function() { dryA.x });
+ assertThrows('dryB.m()', /revoked/, function() { dryB.m() });
+ };
+
+ // test whether the prototype is also wrapped
+ TESTS.testProtoWrapping = function(makeMembrane) {
+ var wetA = { x: 42 };
+ var wetB = Object.create(wetA);
+
+ assert(Object.getPrototypeOf(wetB) === wetA,
+ 'Object.getPrototypeOf(wetB) === wetA');
+
+ var membrane = makeMembrane(wetB);
+ var dryB = membrane.target;
+ var dryA = Object.getPrototypeOf(dryB);
+
+ assert(wetA !== dryA, 'wetA !== dryA');
+ assert(wetB !== dryB, 'wetB !== dryB');
+
+ assert(dryA.x === 42, 'dryA.x === 42');
+ membrane.revoke();
+ assertThrows('dryA.x', /revoked/, function() { dryA.x });
+ };
+
+ // test whether typeof results are unchanged when
+ // crossing a membrane
+ TESTS.testTypeOf = function(makeMembrane) {
+ var wetA = {
+ obj: {},
+ arr: [],
+ fun: function(){},
+ nbr: 1,
+ str: "x",
+ nul: null,
+ udf: undefined,
+ bln: true,
+ rex: /x/,
+ dat: new Date()
+ };
+ var membrane = makeMembrane(wetA);
+ var dryA = membrane.target;
+
+ Object.keys(wetA).forEach(function (name) {
+ assert(typeof wetA[name] === typeof dryA[name],
+ 'typeof wetA['+name+'] === typeof dryA['+name+']');
+ });
+ };
+
+ // test observation of non-configurability of wrapped properties
+ TESTS.testNonConfigurableObservation = function(makeMembrane) {
+ var wetA = Object.create(null, {
+ x: { value: 1,
+ writable: true,
+ enumerable: true,
+ configurable: false }
+ });
+
+ assert(wetA.x === 1, 'wetA.x === 1');
+ assert(!Object.getOwnPropertyDescriptor(wetA,'x').configurable,
+ 'wetA.x is non-configurable');
+
+ var membrane = makeMembrane(wetA);
+ var dryA = membrane.target;
+
+ // perhaps surprisingly, just reading out the property value works,
+ // since no code has yet observed that 'x' is a non-configurable
+ // own property.
+ assert(dryA.x === 1, 'dryA.x === 1');
+
+ // membranes should expose a non-configurable prop as non-configurable
+ var exactDesc = Object.getOwnPropertyDescriptor(dryA,'x');
+ assert(!exactDesc.configurable, 'exactDesc.configurable is false');
+ assert(exactDesc.value === 1, exactDesc.value === 1);
+ assert(exactDesc.enumerable, 'exactDesc.enumerable is true');
+ assert(exactDesc.writable, 'exactDesc.writable is true');
+
+ assert(dryA.x === 1, 'dryA.x === 1');
+ };
+
+ // test non-extensibility across a membrane
+ TESTS.testNonExtensibility = function(makeMembrane) {
+ var wetA = Object.preventExtensions({x:1});
+ assert(!Object.isExtensible(wetA), 'wetA is non-extensible');
+
+ var membrane = makeMembrane(wetA);
+ var dryA = membrane.target;
+
+ assert(dryA.x === 1, 'dryA.x === 1');
+
+ assert(!Object.isExtensible(dryA), 'dryA is also non-extensible');
+
+ var dryDesc = Object.getOwnPropertyDescriptor(dryA,'x');
+ assert(dryDesc.value === 1);
+
+ assert(Reflect.hasOwn(dryA,'x'));
+ };
+
+ // test assignment into a membrane
+ TESTS.testAssignment = function(makeMembrane) {
+ var wetA = {x:1};
+ assert(wetA.x === 1, 'wetA.x === 1');
+
+ var membrane = makeMembrane(wetA);
+ var dryA = membrane.target;
+
+ Object.defineProperty(dryA,'y',
+ { value:2,
+ writable:true,
+ enumerable:true,
+ configurable:true });
+ assert(dryA.y === 2, 'dryA.y === 2');
+
+ assert(dryA.x === 1, 'dryA.x === 1');
+ dryA.x = 2;
+ assert(dryA.x === 2, 'dryA.x === 2');
+
+ membrane.revoke();
+
+ assertThrows("dryA.x = 3", /revoked/, function() { dryA.x = 3; });
+ };
+
+ // test definition of a new non-configurable property on a membrane
+ TESTS.testNonConfigurableDefinition = function(makeMembrane) {
+ var wetA = {};
+ var membrane = makeMembrane(wetA);
+ var dryA = membrane.target;
+
+ function defineProp() {
+ Object.defineProperty(dryA,'x',
+ { value:1,
+ writable:true,
+ enumerable:true,
+ configurable:false });
+ }
+
+ // membranes should allow definition of non-configurable props
+ defineProp();
+ assert(dryA.x === 1, 'dryA.x === 1');
+ assert(wetA.x === 1, 'wetA.x === 1');
+ };
+
+ // test that a membrane preserves object identity
+ TESTS.testIdentitySimpleMembrane = function(makeMembrane) {
+ var wetA = {};
+ var wetB = {x:wetA};
+ var membrane = makeMembrane(wetB);
+ var dryB = membrane.target;
+
+ var dryA1 = dryB.x;
+ var dryA2 = dryB.x;
+ assert(dryA1 === dryA2, 'dryA1 === dryA2');
+ };
+
+ // test that a membrane properly unwraps a value when crossing the
+ // boundary wet->dry and then dry->wet, instead of doubly wrapping the value
+ TESTS.testCrossingSimpleMembrane = function(makeMembrane) {
+ var wetA = {
+ out: {},
+ id: function(x) { return x; }
+ };
+
+ var membrane = makeMembrane(wetA);
+ var dryA = membrane.target;
+ var dryB = dryA.out;
+ var dryC = {};
+
+ var outWetB = dryA.id(dryB);
+ assert(dryB === outWetB, 'dryB === outWetB');
+
+ var outWetA = dryA.id(dryA);
+ assert(dryA === outWetA, 'dryA === outWetA');
+
+ var outC = dryA.id(dryC);
+ assert(outC === dryC, 'outC === dryC');
+ };
+
+ // TODO: test operations other than simple property access/update:
+ // delete, in-operator, for-in, etc.
+
+ // simple test driver loop
+ function runTests() {
+ for (var testName in TESTS) {
+ print("test: " + testName);
+ TESTS[testName](makeMembrane);
+ print("ok");
+ }
+ print("done");
+ }
+
+ runTests();
+
+}());
Please sign in to comment.
Something went wrong with that request. Please try again.