littledan / proposal-proxy-transparent

Transparent Proxies--tunneling private fields, etc.
8 stars 0 forks source link

Counter proposal: add proxy hook to resolve `this` #5

Open dead-claudia opened 6 years ago

dead-claudia commented 6 years ago

Originally suggested here.

Edit: /cc @caridy (Forgot to ping you about this. My bad.)

Edit 2: Remove some less useful parts.

My thought is this: we could add a new essential internal method and corresponding proxy hook to resolve the base this used to invoke getters, setters, and methods. It would be invoked on each call with the key and receiver, so it can determine the proper this to invoke the method with.

And necessarily, [[Get]] and [[Set]] have to be updated to properly handle resolving receivers:

Proxies implementing resolve to return the target are not recommended to expose themselves as subtypable because of all the edge cases around proper subtyping, but they may choose to, and the proposal admits this possibility.


This would address the problem with membranes via the following:

In code, this is how you make a proxy a membrane:

Proxy.transparent = (target, hooks = {}) =>
    new Proxy(target, {...hooks, resolve: target => target})

It is also probably the most flexible, as it lends itself well to a few other possibilities:

Details about `super` being defined as an exotic object This isn't actually being proposed currently, but you could in theory convert `super` within classes into an `arguments`-like construct this way. Note that `super` as defined below would be generated per-call, just like `arguments` is today. Fields for each `super` exotic object *O*: - [[Environment]] is the lexical environment *O* originated from. - *O*.[[Prototype]] is set to the active prototype used. Within static methods, this is set to the value used within the `extends` clause, but within instance methods, it's the value's `prototype` property. - *O*.[[Extensible]] is set to `false`. - No extra data properties are set, so the object is effectively an empty, frozen object. - Note: all of these properties are constant. For each instance `super` exotic object *O*, all the essential internal methods are as specified for ordinary objects, except *O*.[[ResolveReceiver]] is set as per below: - *O*.\[[ResolveReceiver]](*target*, *key*, *receiver*) 1. Return *O*.[[Environment]].GetThisBinding(). For each constructor `super` exotic object *O*, [[SuperConstructor]] is the parent superclass, the value used within the `extends` clause, all the internal fields and methods are set as for instance `super` exotic objects, and [[Call]] is defined as below: - *O*.\[[Call]](*ignored*, *args*): 1. Let *env* be *O*.[[Environment]]. 1. If ! *env*.HasThisBinding() is `true`, throw a `TypeError` exception. 1. Return *env*.BindThisValue(? Construct(*O*.[[SuperConstructor]], *env*.[[NewTarget]], *args*)). In proxy form, where `env` is the environment, `Parent` is the superclass, and `proto` is the `super` base, here's what it'd roughly look like: ```js new Proxy(Object.preventExtensions(Object.create(proto)), { apply(target, thisArg, args) { if (env.this != null) throw new TypeError("`super` has already been called") env.this = Reflect.construct(Parent, env["new.target"], args) return env.this }, resolve(target, key, receiver) { if (env.this == null) throw new TypeError("`this` is not yet initialized") return env.this }, }) ``` Doing this would mean `super` would no longer need very much special treatment from the spec. Things like [IsSuperReference](https://tc39.github.io/ecma262/#sec-issuperreference), [[[HomeObject]]](https://tc39.github.io/ecma262/#table-16), and [GetSuperBase](https://tc39.github.io/ecma262/#table-17) would no longer need to exist. - IsSuperReference would become irrelevant - duck typing is sufficient. - [[HomeObject]] is the [[Prototype]] for `super` exotic objects above. - GetSuperBase would become irrelevant - [[ResolveReceiver]] would fill in that functionality. - [MakeSuperPropertyReference](https://tc39.github.io/ecma262/#sec-makesuperpropertyreference) would be replaced with just setting up `super` appropriately when invoking a method, at the same time as `this` and `arguments`. Most of the existing syntactic rules around `super` would be simplified to just define `super` as similar to `new.target` or `this`. And the lexical stuff would treat `super` as similar to `this`.

If you're curious about the performance implications of this, engines can drop ICs to detect proxies and whether they set resolve. If they do, it's an obvious candidate for inlining most of the time, and can be inlined like any other proxy method. In bytecode, it might be slower if engines don't check for the "return the first/third argument" pattern, but I doubt that'll be a major problem even if not implemented. (The issue only exists for proxies.)

caridy commented 6 years ago

@isiahmeadows this is pretty much the same as in #4, which I didn't detailed very well but certainly it needs the new internal slot. There are few differences that I will like to clarify:

dead-claudia commented 6 years ago

@caridy Mine is similar to that proposal, but not quite the same. Here's the two main concrete differences:

The first opens up a few other use cases beyond simple unwrapping, like multiple delegation or other computed, property-specific receivers. The second is just simplifying the unwrapping, preferring to do less for something called on basically every property access.

  • why O.[[ResolveReceiver]](key, receiver) needs a key? IMO, this operation should be about what object should the engine use for a certain operation, not about what operation is the engine about to performance on that object.

The resolve proxy hook uses the key, hence why it's there. As for why that key could be useful, I stated an example in the initial comment (emphasis mine):

A proxy might wish to delegate to two different targets, depending on the key. Instead of defining a very weird and complicated get and set, it could choose to implement getOwnPropertyDescriptor to return the relevant descriptor, resolve to return the relevant target as the receiver, and ownKeys to return the two sets of keys merged together.

That's one example where the key would be useful. This proposal was designed to enable more than simple membranes, but also be flexible enough for things like emulate multiple delegation without actually adding that to the language. Just giving the building blocks for it is all that's necessary here.

  • why do you think this is something observable from outside? Why do you need Reflect.resolve?

That method was primarily to be consistent with all the other methods. I'm not married to the idea personally and I removed it from the main proposal, if that helps.

  • I'm curious about the new proxy invariant that you're suggesting. Why? perf? remember that the proxy handler is mutable (a mistake from the past that we keep regretting), so I wonder why is this such a big problem?

The invariant is to be consistent with getOwnPropertyDescriptor - when calling non-configurable own methods, you'd expect both the descriptor and the receiver to remain the same on each call. Anything else would seem extremely bizarre, which is why I banned it.


I'm not married to the idea of passing the key, so I'm okay with that getting removed, in which it basically becomes #4.

caridy commented 6 years ago

@isiahmeadows, thanks for the explanation. I think I can narrow the proposal now. #4 propose introducing a new mechanism that is generic, it is not about proxies only, the unwrapping mechanism can potentially be used in other scenarios. From that point of view, the mechanics to achieve the unwrap should not take into consideration neither the proxy itself or keys, or anything else other than the object to be unwrapped. And yes, proxies will tap into those mechanism to support proxy unwrapping.

The invariant is to be consistent with getOwnPropertyDescriptor

I need to think more about this and talk to @erights on Thur about this particular question. At first glance unwrap seems harmless, similar to shadow target mechanism, but there might be something that I'm not seeing here.

dead-claudia commented 6 years ago

@caridy As an alternative, you can just make it so when accessing descriptors for non-configurable properties, you only call unwrap the first time for each key and memoize it as part of the descriptor, never to call it again for that key. That avoids the need to check the invariant while still remaining consistent with getOwnPropertyDescriptor, and it's probably faster. It also works better with #4 by avoiding having to expose nearly as much behavior around the key.