-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ergonomics on WrappedFunction thisArgument throwing when it is an object #328
Comments
I'm supportive of this reasoning. I'm curious what @erights thinks of it? |
Yes, I also support it, but only with the current conclusion: that the Note that this concerns only the |
What happens if i do |
Would people really expect that? These functions are already special and don't let non-primitive arguments or return values through. We can very well say they don't support passing any |
It's the way JS works already, and TC39 took the time to make that change when adding strict mode, so it seems like it should require a large burden to change it. This proposal's callable are already bizarre in a number of ways; it would be great to reduce the otherwise-valid use cases they don't work with. |
@ljharb We can set |
sounds like just changing the following in the wrapped function -7. Let wrappedThisArgument to ? GetWrappedValue(targetRealm, thisArgument).
-8. Let result be the Completion Record of Call(target, wrappedThisArgument, wrappedArgs).
+7. Let result be the Completion Record of Call(target, undefined, wrappedArgs). expanding the case above things seem ok: const shadowRealm = new ShadowRealm();
const whatIsThisWrapped = shadowRealm.evaluate(`
(function whatIsThis() {
return this === globalThis;
});
`);
const whatIsThisStrictWrapped = shadowRealm.evaluate(`
(function whatIsThis() {
"use strict";
return this === undefined;
});
`);
const store = {
whatIsThisWrapped,
whatIsThisStrictWrapped
};
store.whatIsThisWrapped(); // true
store.whatIsThisStrictWrapped(); // true As pointed out by @legendecas, the last two lines above are a TypeError today just because the wrapped function being set as the value of an object property. The spec change also allows: whatIsThisWrapped.call([]); // true
whatIsThisStrictWrapped.call([]); // true While the only downside is not being able to transfer a this value to the other realm: whatIsThisWrapped.call(1); // true
whatIsThisStrictWrapped.call(1); // true |
But it's not a change. It's a different behavior for a new feature / addition. One doesn't expect all functions, to receive an explicit Code has to explicitly receive a function from another
What would the lexical |
To clarify, I'm not particularly in favor of one or the other. IMO this is a low level API, and the program has to have different expectations when working with these wrapped functions:
|
@mhofman I was wrong and the other spec change is way more effective than setting In any case, not observing the |
Agreed. There are pros and cons for both sides, but for what I want in the wrapped functions everything seems low level and I'm aware that wrapped just chains the argument values to another function call inside the shadowrealm and get the result. I don't feel like we should encourage one to also transfer values using |
Wrapping a non-arrow function and getting arrow function semantics seems very strange to me - especially if the function uses its own prototype inside realm, which would be totally valid. Imagine, inside the realm: function f() {
return !!f.prototype
}; Inside the realm, calling If not (and i'd hope not), then it's not an arrow function, and it can't be explained as "it's like an arrow function, that's why |
It can be explained as a function automatically bound to |
Borrowing semantics similar to arrow function work just as finding precedent, but it doesn't mean the wrapped functions will be presented like these. @mhofman said they can be considered as bound to undefined. |
That explanation holds, at least, but that still doesn't explain why we'd want to prevent something that could otherwise work. |
@ljharb There are pros and cons, and I prefer what this change would provide. With the proposed change we can place the wrapped function as values of objects and avoid surprises such as The status quo allow setting custom this values as long as they are primitives or callables, but still can't set // status quo
store.foo(); // TypeError
store.foo.call(undefined); // works
store.foo.call(1); // works
store.foo.call([]); // TypeError
class Store {
static foo = store.foo
}
Store.foo(); // works, not a TypeError with the current spec text
store.foo.call(store.foo); // works
store.foo.call(function() {}); // works
store.foo.bind(undefined)(); // works
const boundFoo = store.foo.bind(store); // works!!
boundFoo(); // TypeError With the proposed change, all the TypeErrors listed above are removed, but the this value will be ignored when calling the target function. |
Is it possible to make it a runtime error when the wrapped function tries to access const a = {
foo: realm.evaluate('function () { this }'),
foo2: realm.evaluate('function () { return 1 }')
}
a.foo2() // 1
a.foo() // TypeError |
@Jack-Works I didn't find throwing on accessing |
In the spec, I don't see anything that can help us here except for a revoked proxy, but the incoherence with arguments, which are going to be wrapped before calling the function and throwing if needed, and the |
@Jack-Works it seems like your suggestion would need a separate and probably more challenging discussion. I agree with @legendecas and @caridy and I don't want it for this API. This would require shadowrealms evaluating the function body of anything returned/wrapped. I believe the challenge of this issue is changing the wrapped function to ignore While the champions showed positive interest in this change, it seems we need consensus from @ljharb who needs to be onboard with it. I'd like to pull the discussion back to this topic. |
I confirmed with @ljharb we don't have consensus for this change yet. I don't think this would or should block the proposal, but I hope we can eventually work through accepting the change. |
I have a question, which I assume I am wrong about, but wanted to ask it anyway: Currently I am having trouble with a proxy and private class fields. wouldn't the wrapping and/or removing of the |
@chris-kruining Nothing with a private field will be able to cross a shadow realm and end up being an object with the same private field - the issue you’re referring to is an unavoidable one with or without this change. |
I’ve experimented with adapting some existing iframe-realm-based code to ShadowRealm in D8 in the last few weeks and have found this behavior to be nothing but (very frequent) surprise pain in practice. I can imagine scenarios where it could be desirable, but if this behavior is to remain, please consider making WrappedFunctionCreate / WFEO.[[Call]] take into account the target function’s
|
That's an interesting idea. To make sure I got that right, if the target of the WFEO is an arrow function, then it wouldn't try to wrap the this value and just ignore it? I'd have to think if this would expose any new capability to sense what kind of function you're holding. |
@bathos I think I'm fine with that suggestion since the arrow function can't really observe the
|
That’s true — though it seems like it’d stop short of constituting a real “generalizable” brand check right? Like, the kind suitable for use with arbitrary values about which you hold no prior knowledge. The receiver conversion step is currently the last point in WFEO.[[Calll]] that can be reliably caller-observed before WFEO.[[WrappedTargetFunction]].[[Call]] is entered. If that same step became conditional (but did not move relative to the others), the last point that can be caller-observed also becomes conditional and so is no longer reliable. So a novel observation is possible, but not an observation that you can be sure in advance will not have unknown external effects. If you (GetFunctionRealm happens before any of the conversions, so you can already write a reliable “does this WFEO’s [[WrappedTargetFunction]] function terminate with a revoked PEO” test using WFEO.[[Call]]. Previous language features already left this status reliably observable by other means, so it’s not a problem AFAIK, or avoidable, but it’s the same order-of-observable-ops-reveals-information-about-a-slotted-value thing via function arguments that observe their own conversion via "name" and "length" properties). In any case I’m hesitant about my own suggestion due to likely-still-common code transformations from |
|
One issue with detecting the target's mode at call time is that it'd require recursion for nested WFEO. I suppose we could detect the target's mode at creation time and store a "target ignores this value" flag in a slot of the WFEO. Then when detecting the target at creation, it'd set the flag for BFEOs or |
That’s a particularly interesting example. Web IDL’s “regular operations” implement “undefined → global” behavior similar to sloppy functions so that if they are members of a Note that my prior suggestion would not help with |
I fail to see why not? |
@mhofman sorry, that was unclear: it would help with the example given because that example is about how setTimeout passes a receiver that’s an object when invoking the callback. What it would not help with is setTimeout itself, which is receiver-sensitive, too, but treats undefined as though it were its realm’s global. shadow.evaluate("provisioned => { globalThis.setTimeout = provisioned }")(setTimeout);
// in the shadow later, this throws in sloppy mode even if arrows were exempted from receiver-mapping:
setTimeout(() => {}, 0); though I suppose that issue is not actually unique to global interface ops, thinking about it more |
Right, I believe all host functions are exotic, so I would not expect them to fall under this intervention. Same with proxies of functions. At the end of the day this is to make the user's bound and arrow functions more ergonomic through the callable boundary, nothing more. Of course I'm still open to simply saying the receiver doesn't get wrapped and becomes undefined, but that does raise other issues. For example, a wrapped sloppy function (aka |
As for now,
WrappedFunction.[[Call]]
converts thethisArgument
to wrapped value and then passes the wrapped this argument to the target function in the other realm. When this behavior comes to life, the following common pattern can be very awkward:So people have to make sure they call the wrapped function without this binding, like:
This makes calling the wrapped function less ergonomic and cumbersome to be correct. Since the wrapped function can not be called with
new
as it doesn't have[[Construct]]
and it doesn't make sense to return an object fromnew
operator for wrapped function, maybe we should omit thethisArgument
and bindthis
with dedicated values likeundefined
?The text was updated successfully, but these errors were encountered: