tvcutsem / harmony-reflect

ES5 shim for ES6 Reflect and Proxy objects
http://www.ecma-international.org/ecma-262/6.0/#sec-reflection
Other
478 stars 48 forks source link

Get/Set receivers don't work with inheritance in v8 #4

Closed polotek closed 11 years ago

polotek commented 12 years ago

I'm sure this is a known issue. The v8 legacy proxy implementation seems to have a bug with get and/or set. When using a proxy as a prototype, the get and set traps should get the actual receiver of the property access as an argument. But that doesn't seem to be the case. The traps always receive just the proxy object. Here's a test.

// for node.js
if(typeof require == 'function') {
  var load = require;
}

load('../reflect.js');

var RCVR = null;

function test() {
  var target = {}
  var proxy = Proxy(target, {
    get: function(target, name, receiver) {
      console.log('GET');
      RCVR = receiver;
    },
    set: function(target, name, val, receiver) {
      console.log('SET');
      RCVR = receiver;
      return true;
    }
  });
  var child = Object.create(proxy);

  child.foo;
  console.log('get :' + (RCVR === child));

  child.bar = 'bar';
  console.log('set :' + (RCVR === child));
}

I spent some time trying to come up with a way to work around this bug in v8 to get inheritance working with this proxy shim. But I can't see one. this is really unfortunate because without this, nice use cases like MethodSink just don't work. Should I file this bug with v8? It's unlikely that they'll fix it considering the old proposal is likely going away.

There may also be something wrong with this test. I ran it in FF (where all the current tests pass) and it fails there. For some reason the get/set traps don't seem to be called at all in FF. I don't get the GET output that confirms the trap. Any idea what I'm doing wrong?

ghost commented 12 years ago

Hmm I'm not seeing this with V8. What version are you using? There was a lot more issues in the V8's that node had during 0.6.x. This code for a similar but different library works correctly:

var proxied = meta.proxy({}, {
  get: function(fwd, target, key, rcvr){
    console.log(key, rcvr === proxied);
    return fwd();
  },
  set: function(fwd, target, key, value, rcvr){
    console.log(key, rcvr === proxied);
    return fwd();
  },
});

proxied.test = {};
// test - true

var child = Object.create(proxied);

child.test;
// test - false
ghost commented 12 years ago

That is the proxy/weak map implementation in pre 3.8 V8 had issues.

polotek commented 12 years ago

I've run this test in node 0.7.8 using v8 3.9.24.9, and also in Chrome Canary 21.0.1138.0 using v8 3.10.8.4. Same incorrect results. It looks like the get trap gets the right receiver, but the set trap does not.

ghost commented 12 years ago

Wow you're right. This explains some of the issues I've had. I never noticed because it only works on the setter.

tvcutsem commented 12 years ago

Proxies & inheritance are a mess in the current Proxy implementations. In ES6, we plan to clean up this mess with an update to the way inheritance is specced. This is a separate proposal from proxies, which impacts the "get", "set" and "has" traps on a Proxy acting as the prototype of another object.

Here's the current state as I can piece it together, for both SpiderMonkey and V8.

SpiderMonkey

In Spidermonkey, when performing a get or set on a child object that inherits from a proxy prototype (as in the above example), if child does not have the corresponding property, the implementation will check whether the property is defined somewhere higher up in the proto chain as if by calling name in proxy. This will trigger the proxy's has() trap.

@polotek, the reason why you see the tests fail in FF is probably because you didn't add a has trap. Try adding a has trap that always returns true, as in has: function() { return true; }. The tests should then pass.

V8

In V8, the implementation does not call has() on the proxy to test whether the property exists, but instead immediately calls getPropertyDescriptor. This correctly triggers the inherited get/set traps (at least in v8 3.11.0)

However, in the case of set, the value of this wrongly points to the proxy rather than to child. This seems like a genuine bug in v8. I checked whether it also occurs for regular objects (i.e. have child inherit from a normal object that defines a getter/setter), but that seems to work fine.

in-operator

Related to all of this: in ES5, the in-operator is internally specced to use the getPropertyDescriptor trap (see ES5 8.12.6). Because this proxy shim "abuses" the deprecated getPropertyDescriptor trap to always return fake accessor properties, to make inherited get/set traps work, it does break the in-operator as follows:

When performing name in child, the result will always be true. If name exists in child, the result is obviously true. If it doesn't, the implementation will search for name in the prototype chain and call getPropertyDescriptor on the proxy prototype. This trap will return a fake descriptor, which makes the in-operator return true.

summary

So, the current state of things is:

@polotek the MethodSink abstraction only depends on get, not set, so that should work fine in both SM and V8 if you make the MethodSink's has() trap return true unconditionally.

ghost commented 12 years ago

Oh this will help me figure out some issues I was having trying to make a DOM wrapping membrane that works in V8 but fails in Spidermonkey. The issue there being that xpconnect wrappers provide DOM properties via their prototypes.

On the whole, your first sentence pretty much sums it up. Proxies work more or less but aside from the above mentioned bugs, they are just overall unstable and seem to unexpectedly cause the engines to stop functioning without much in the way of error messages. It's understandable though, since proxies expose things that previously never touched JS-land code or went through previously different paths.

ghost commented 12 years ago

There's another issue with V8 that I finally boiled down to this simple test case. Basically, the keys and getOwnPropertyNames traps cannot report any properties that exist in Object.prototype. Even if the proxy's prototype is null.

function broken(o){
  return Proxy.create({
    getOwnPropertyNames: function(){
      return Object.getOwnPropertyNames(o);
    },
    keys: function(){
      return Object.keys(o);
    },
  });
}

Object.keys(broken({ toString: function(){} }));
//TypeError: Trap 'undefined' returned repeated property name 'toString'

Object.getOwnPropertyNames(broken({ toString: function(){} }));
//TypeError: Trap 'getOwnPropertyNames' returned repeated property name 'toString'
tvcutsem commented 12 years ago

@Benvie thanks for reporting. I filed this bug with v8. This seems like something that's easily fixable.

ghost commented 12 years ago

Looks like both the setter bug and keys error bugs will be fixed in the next V8 release: https://chromiumcodereview.appspot.com/10451064/ https://chromiumcodereview.appspot.com/10453053/

ghost commented 12 years ago

The fixes for both issues landed in V8 3.11.8 http://code.google.com/p/v8/source/browse/trunk/ChangeLog?spec=svn11689&r=11689

tvcutsem commented 12 years ago

Thanks for the heads-up. V8 3.11.9 now correctly passes the tests in test/testProxies.js, except for one:

fail: invoking inherited has on non-existent prop ( assertion on line 641 )

As noted before: on v8, applying the in operator to an object that inherits from a proxy doesn't trigger that proxy's has() trap but instead always returns true. This is due to the fact that the in operator still uses the now deprecated getPropertyDescriptor() trap. I don't see an obvious work-around for now.

billmark commented 11 years ago

With V8 3.11.10.25, in node.js v0.8.14, the following seems to demonstrate a Direct Proxies bug. Is this a known issue?

"use strict"; var Reflect = require('harmony-reflect'); var target = new Date(1969,10,3,8,0,0,0); var handler = {}; var proxy = Proxy(target, handler); console.log('target.getDate = '+target.getDate()); console.log('proxy.getDate = '+proxy.getDate()); // Bug - Fails with "TypeError: this is not a Date object."

billmark commented 11 years ago

I now see that it is a known issue. It is listed under "Date, RegExp, some Array prototype built-ins fail on proxies". Sorry for posting in the wrong place.

tvcutsem commented 11 years ago

The in operator now works correctly in v8 when applying it to a child object inheriting from a proxy:

var p = new Proxy({}, {});
var c = Object.create(p);
print('foo' in c); // used to print true, is now false

AFAICT this is the last issue w.r.t. inheritance and proxies, so I'm closing this issue.