02:17
<Jack Works>
Unlucky
02:17
<Jack Works>
What's the reason it being rejected?
03:28
<devsnek>
it was considered to be adding too much complexity
03:28
<devsnek>
ultimately i think that was a mistake (i still see people run into the dynamic import problem in the wild) but what can ya do 🤷
03:30
<devsnek>
hmm here are the notes https://github.com/tc39/notes/blob/main/meetings/2018-05/may-24.md#symbolthenable-for-stage-1-or-2
04:04
<Mathieu Hofman>
Is it possible to add a Symbol.unthenable and attach it to the module namespace object?
I think unfortunately that ship has definitely sailed. At this point adding such a symbol to the language wouldn't accomplish anything. You can't add such a symbol automatically to all module namespace objects as it'd be Web incompatible. And developers can't add such a symbol themselves (only string identifiers can be exports) before they'd get tripped by a dynamic import.
07:26
<Rob Palmer>
Just some history... the hazard of dynamic import resolving thenables was known ahead of time. I tried to point it out but it didn't end up going anywhere as it was framed as yet-another-instance of an existing hazard. https://github.com/tc39/proposal-dynamic-import/issues/48
18:59
<ljharb>
I think unfortunately that ship has definitely sailed. At this point adding such a symbol to the language wouldn't accomplish anything. You can't add such a symbol automatically to all module namespace objects as it'd be Web incompatible. And developers can't add such a symbol themselves (only string identifiers can be exports) before they'd get tripped by a dynamic import.
why would it be web incompatible? exports can't provide symbols there, specifically so we CAN add a new symbol to module namespace objects at any time.
19:00
<ljharb>
or do you just mean, making it unthenable would be incompatible
19:01
<Mathieu Hofman>
I meant adding a symbol such as unthenable by default to all module namespace objects. There are modules in the wild that do expect this thenable behavior
19:01
<ljharb>
right, true enough. altho probably not a ton, since having a thenable module is broken in node (i forget if it's broken just in the repl, or just not in the repl)
19:04
<Mathieu Hofman>
It definitely works in the browser, I have personally (ab)used this mechanism
19:41
<Justin Ridgewell>
Ooo boy, did anyone realize we don't actually follow the Promises A+ spec?
19:42
<Justin Ridgewell>
We pass the spec's tests, only because it forgot to test a behavior.
19:42
<Justin Ridgewell>
Step 13-15 of https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promise-resolve-functions
19:43
<Justin Ridgewell>
We create a new job to handle a thenable passed to resovle in new Promise(res => res(thenable))
19:43
<Justin Ridgewell>
That's not correct
19:43
<Justin Ridgewell>
https://promisesaplus.com/#point-56
19:44
<Justin Ridgewell>

If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where...

19:44
<Justin Ridgewell>
It makes no mention of a new job for thenables
19:45
<Justin Ridgewell>
This is the root cause of both https://github.com/tc39/ecma262/issues/2770 and https://github.com/tc39/ecma262/issues/1250
19:47
<Justin Ridgewell>
thenable.then(resolve) and resolve(thenable) are supposed to take the same number of ticks.
19:48
<Justin Ridgewell>
But because resolve(thenable) creates a new job for doing the then.call(prom, onFul, onRej) and the then internally creates a new job before calling onFul/onRej, we get 2 ticks instead of 1.
20:05
<Mathieu Hofman>
It might be tricky to untangle this. Promise.resolve() should most likely not call .then in the same tick (It's already unfortunate it does a [[Get]] of the constructor and then). Maybe there is a way to not use a new callback job if we're already inside then callback job? I haven't thought this through yet.
20:10
<Justin Ridgewell>
Are you thinking about throws?
20:11
<Justin Ridgewell>
Note that Promise.resolve isn't part of A+, so we could diverge and do an tick before invoking the thenable's then
20:11
<Mathieu Hofman>
I'm wondering how you could test this though. The .then call would always happen when no user code is on the stack. Maybe the test could create a resolved promise and attach a fulfillment handler (as a "next tick" detection) before returning the test thenable, and ensure that the .then is called before the "next tick" ?
20:14
<Justin Ridgewell>

It's easy, I think:

new Promise(res => {
  let sync = true;
  res({
    then(onFul) {
      onFul(called);
    }
  });
  sync = false;
});
20:15
<Mathieu Hofman>
ah right, the constructor calls the resolve step, silly me
20:16
<Justin Ridgewell>
Can you explain why Promise.resolve(thenable) shouldn't call thenable.then sync?
20:18
<Mathieu Hofman>
It's just a non-obvious reentrancy hazard. Basically the only way to protect yourself from an evil promise is to do Promise.resolve(() => thenable)
20:18
<Justin Ridgewell>
Promise A+ handles reentrancy
20:19
<Mathieu Hofman>
Which means you're always losing a tick when you don't know where a potential thenable is coming from.
20:19
<Mathieu Hofman>
I'm talking about user code trying to protect itself from reentrancy induced by evil promises
20:19
<Justin Ridgewell>
The onFul and onRej passed to the thenable's then are already job deferred callbacks
20:20
<Justin Ridgewell>
They can only be called once
20:20
<Justin Ridgewell>
And if then throws, then it'll be caught and onRej will be called (if neither onFul nor onRej have already been called)
20:21
<Mathieu Hofman>
Aye, we're not talking about the same thing.
20:24
<Mathieu Hofman>
calling fooResult.then() can cause synchronous reentrancy in your code if you can't trust where fooResult comes from. And there is simply no way to brand check for a native promise without causing similar hazard (Promise.resolve will trigger constructor and then get logic)
20:25
<Justin Ridgewell>
Sorry, I'm still not getting it
20:26
<Justin Ridgewell>
If you can't trust fooResult, then Promise.resolve(fooResult) would give you a trusted promise (and I assume you're already doing this because fooResult isn't trusted)
20:27
<Justin Ridgewell>
So we haven't introduced a new tick?
20:27
<bakkot>
I don't think code should be trying to protect itself from reentrancy induced by evil promises
20:27
<bakkot>
except in extremely unusual cases
20:28
<Mathieu Hofman>
Promise.resolve({ get then() { doSomethingEvil(); return () => {}; } }) will call doSomethingEvil synchronously.
20:28
<Mathieu Hofman>
I don't think code should be trying to protect itself from reentrancy induced by evil promises
Depends on what kind of code you're working on ;)
20:29
<bakkot>

except in extremely unusual cases

20:30
<Justin Ridgewell>
Given get then() { doSomethingEvil() }, is sync calling then() { doSomethingEvil() } something we need to protect against?
20:32
<Mathieu Hofman>
Anyway, we're off topic for Justin's topic. I just wish we had a way to brand check native promises so that something like brandCheck(p) && Reflect.getPrototypeOf(p) === Promise.prototype && !Reflect.getOwnPropertyDescriptor(p) would be sufficient to know I can safely call p.then
20:33
<Mathieu Hofman>
(given that Promise.prototype is frozen of course)
21:04
<ljharb>
yes, it is, because Promise.resolve({ then: class {} }) shouldn't throw, it should reject
21:07
<Justin Ridgewell>
It would still reject with the correct A+ behavior
21:07
<Justin Ridgewell>
Throws are caught
22:56
<ljharb>
i'd love a stamp on https://github.com/tc39/notes/pull/197 if someone has a sec
23:07
<rbuckton>
Rather than adding a symbol to the module, could we allow a user to add a built-in symbol to a function they export named then that would indicate it shouldn't be used for Promise fulfillment? Non-native promises might not understand it, but it would still work for imports.
23:24
<Mathieu Hofman>
Interesting idea. But that's a protocol that would have to work for every thenable, not just module namespaces. And that means one more lookup during promise resolve.
23:59
<Jack Works>
I think unfortunately that ship has definitely sailed. At this point adding such a symbol to the language wouldn't accomplish anything. You can't add such a symbol automatically to all module namespace objects as it'd be Web incompatible. And developers can't add such a symbol themselves (only string identifiers can be exports) before they'd get tripped by a dynamic import.
What if all modules already have @@thenable: true and you can opt out with
js import * as self from './file.js' self[Symbol.thenable] = false