03:36 | <Steve Hicks> | we have to have this conversation with Steven and Signals people about Computed's context -- there's a concern that call-time context there would constitute "Zalgo": a computed signal could be forced in many different ways, and it should be giving the same answer regardless of context (of course we need some debugging/perf analysis tools to be possible) I agree that there's definite problems with computed signals - it's possible that two setters feed into the same computed signal and could have been set in different contexts, plus the fact that they're computed lazily might point to the reader's context as the correct call-time context (i.e. coming from the other direction). So there's potentially three or more different options and not necessarily a good way to disambiguate all of them. That said, if there's no way to access the context(s) that set the signal(s) that caused the recompute, then tracing in many UI frameworks is essentially sunk. In my experience, it's a very common pattern to have an event handler do nothing but update a signal. If the tracing framework initiates a trace in the event handler then the trace would simply die then and there, whereas we'd like to be able to link that trace to any downstream reactions/effects, all the way to the re-render. |
13:18 | <littledan> | I see... so in this case, it's like application state should be registration-based, but tracing state should be call-based... :( |
14:07 | <Steve Hicks> | What's an example of application state that you'd want to be registration-based? My general (but somewhat uninformed) rule-of-thumb is that if a callback is intended to be called multiple times, it's more likely to want call-time context. |
14:11 | <littledan> | an example is if we wanted to use AsyncContext for tracking the owner in tree-based rendering, or generally, React Context-style information |
14:11 | <littledan> | we could use AsyncContext.Snapshot.wrap for these cases but then it'd defeat tracing, sounds like... |
14:12 | <littledan> | also for the style of React Hooks using global variables under the hood |
14:13 | <littledan> | restoring the context after await is an example of registration-time behavior I think |
14:13 | <littledan> | (especially obvious if you consider what explicit calls of .then() should do) |
17:21 | <Steve Hicks> | That's a good point about await . I brought it up in the context of async generators in https://github.com/tc39/proposal-async-context/pull/77#issuecomment-2048897179 - I think my general expectation is that context needs to be preserved across an await , rather than restored to some particular snapshot on reentry |
17:50 | <littledan> | Justin keeps talking about "restoring to the initial snapshot" but I've always been thinking about the semantics as "preserving the one that was right before the await or yield" (that's my mental model for AsyncContext in general). In the end, there isn't an observable difference between them, though. |
17:51 | <Andreu Botella> | well, with a disposable there would be a difference |
17:52 | <littledan> | right, if we had some flat way of doing run within a function, I'd definitely want the semantics to be, preserve what was before the await/yield; I don't see any argument for "restore the one at function entry" |
17:52 | <Steve Hicks> | I think await and yield are maybe separate questions, but otherwise I agree. |
17:53 | <littledan> | is there any post which captures your thoughts on yield? it's been a little hard for me to follow the threads given their length |
17:55 | <Steve Hicks> | I don't really use generators, so I don't have particularly strong feelings on yield, beyond recognizing that that's at least a small handful of people who seem to want to be able to observe the calling context somehow or other. My bigger concern is repeated callbacks, which I think are a little easier to reason about. |
17:56 | <littledan> | on yield, it seems like the biggest users in frameworks would actually benefit from these semantics because they end up using yield as a replacement for await |
17:56 | <littledan> | but I like focusing on the concrete and want to understand more about your thoughts on callbacks |
17:56 | <Steve Hicks> | I think yield-as-await already gets the right behavior automatically based on how promises work. |
17:57 | <littledan> | I think yield-as-await already gets the right behavior automatically based on how promises work. |
17:57 | <Steve Hicks> | (but I haven't thought through the details) |
17:57 | <littledan> | What I've heard is, Koa doesn't force a Promise.resolve the way native await does, and that's where the difference comes from |
17:59 | <Steve Hicks> | For repeated callbacks, it's common to register handlers or data pipelines at application start time. These callbacks run every time a particular interaction or data flow happens. There's no meaningful context when they're registered, so it's much more relevant to propagate the call-time context for each iniating circumstance, rather than the (empty) app-init context. |
18:00 | <Andreu Botella> | For repeated callbacks, it's common to register handlers or data pipelines at application start time. These callbacks run every time a particular interaction or data flow happens. There's no meaningful context when they're registered, so it's much more relevant to propagate the call-time context for each iniating circumstance, rather than the (empty) app-init context. loadend on XHR? |
18:01 | <Andreu Botella> | you can use a single XMLHttpRequest object for multiple fetches, but I don't think most uses do that |
18:09 | <Steve Hicks> | More concretely, we have an internal framework that writes code (very roughly) like this:
In this example, there's a lot of loose coupling: the click handler just fires-and-forgets an RPC. The RPC service has some middleware registered that gets a response of a certain type and then indexes all the cached ItemDetail models by their ID, retrieves the matching detail, and updates its votes. The UI data binding then picks up this change and rerenders the component. What I need to be able to do is thread a trace through that sequence of loosely coupled triggers. The callbacks are all registered once, when the module is loaded, but they need to carry along the call-time context in order to preserve the trace. |
18:10 | <littledan> | (is this the same internal framework that Jatin works on?) |
18:10 | <Steve Hicks> | My understanding is that this sort of loosely-coupled data binding is common in external frameworks as well. Yes, it's the same. |
18:10 | <littledan> | hey how do people feel about getting a logo for AsyncContext like we have for Signals? https://github.com/tc39/proposal-signals/blob/main/signals-logo.png |
18:11 | <littledan> | I can ask my friend who did the other one if he could do this too |
18:11 | <littledan> | somehow logos make it easier to talk about things, sometimes |
18:41 | <rbuckton> | I need to better understand the concern about using and an async context scope. As I understand it, even with [Symbol.enter] you're concerned it's possible exit the function without exiting the scope? |
18:46 | <Andreu Botella> | I need to better understand the concern about [Symbol.enter] but not [Symbol.dispose] would change the context available when the function returns |
18:46 | <Andreu Botella> | currently in the AsyncContext proposal there's no way to change the context in the middle of a function execution |
18:47 | <Andreu Botella> | and I think this was a design goal, which is why we didn't adopt AsyncLocalStorage 's enterWith |
18:47 | <rbuckton> | How does this:
differ from this:
|
18:48 | <rbuckton> | That would also result in a dangling scope, even with some kind of more comprehensive enforcement of using |
18:50 | <Andreu Botella> | the current scope text switches the context when generators are paused and restored – there's still discussion about whether this is the right behavior, but that would take care of that |
18:50 | <littledan> | yeah, neither of those is acceptable, that's the thing |
18:50 | <littledan> | the current API is based on .run(cb) instead |
18:51 | <rbuckton> | If failing to exit the scope is unacceptable, then there's no way to achieve that kind of enforcement with using |
18:52 | <Andreu Botella> | I guess the fact that AsyncContext can switch the scope for generators is a thing that no userland use cases could do, so maybe that doesn't apply more generally |
18:52 | <rbuckton> | Async functions would also result in a dangling scope
|
18:54 | <rbuckton> | This leads me to believe that AsyncContext.Scope isn't viable. |
18:55 | <littledan> | a different thing in the space of using could hypothetically solve this problem--maybe more along the lines of what you described Python can do in with . |
18:55 | <littledan> | not that we need that, but the name of the proposal kinda feels like a tease, almost getting us there |
18:55 | <rbuckton> | Python's with would have the exact same issues. |
18:55 | <Andreu Botella> | This leads me to believe that |
18:56 | <Andreu Botella> | And I'm not willing to ask for the strict using enforcement proposal to change to fit use cases which are limited to spec proposals |
18:56 | <littledan> | in the extreme case: if using was passed the delimited continuation as an argument... |
18:58 | <Steve Hicks> | I think you're missing something here. The issue is only in any shared contexts. The context outside the async function isn't affected by the Scope. I assume the point of Scope is that it effectively makes a child context "in place" - so it wouldn't impact any other function bodies that were sharing the same context as before the Scope is entered. Specifically, it would not mutate the current context. |
18:58 | <rbuckton> | in the extreme case: if |
18:58 | <littledan> | I don't see how this wouldn't be significantly worse. |
18:59 | <littledan> | the design of .run(cb) forces you to extract the delimited continuation manually |
19:01 | <Steve Hicks> | Where I see a problem is
|
19:02 | <rbuckton> | if you were to treat this like a stack, as run(cb) would do, I would expect innerScope to be on top of the stack, and when scope is disposed it should throw if it's not on the top of the stack. |
19:03 | <Andreu Botella> |
innerScope if scope has been disposed. But I'm not sure at this point if this would work for async functions and generators |
19:04 | <rbuckton> | Do you have to keep track of when you enter or exit a function with run() ? |
19:04 | <Justin Ridgewell> | No |
19:05 | <Justin Ridgewell> | Run enters, invokes the cb, then exits. It can’t leave a dirty stack. |
19:11 | <rbuckton> | One way I could envision this behavior would be something like this very naive implementation:
Each time you create a new |
19:12 | <Steve Hicks> | (I'll also point out that this is effectively impossible to polyfill, for whatever it's worth - you'd need to instrument every function with a local variable to store the current context in, rather than relying on a global.) |
19:16 | <rbuckton> | This doesn't require strict enforcement and it throws when used incorrectly. Theoretically, before you throw you could also walk the stack from #top to this and exit those contexts as well, but it's still better to error than to exit silently. |
19:18 | <Justin Ridgewell> | I think that still leaves a dirty stack, just with an error telling you that something went wrong. It doesn’t prevent the misuse from happening. |
19:19 | <Justin Ridgewell> | (I'll also point out that this is effectively impossible to polyfill, for whatever it's worth - you'd need to instrument every function with a local variable to store the current context in, rather than relying on a global.) |
19:19 | <rbuckton> | I think that still leaves a dirty stack, just with an error telling you that something went wrong. It doesn’t prevent the misuse from happening. using doesn't prevent a dirty stack, as I illustrated with the generator and async function example. And as I mentioned, you can clean up the stack before you error. |
19:20 | <Justin Ridgewell> | I disagree, it defintely does |
19:20 | <rbuckton> | ~~not within the context of run .~~ sorry, misread the comment |
19:21 | <rbuckton> | How does using prevent a dirty stack, given the examples I just posted? |
19:24 | <rbuckton> | I assume a generator has a magic behavior that does context capturing when started and resumed? Is there any reason you wouldn't do the same for normal function execution? Or is that just too expensive? |
19:24 | <Justin Ridgewell> | It requires a modification in the internal pausing behavior for await and yield |
19:25 | <Andreu Botella> | I assume a generator has a magic behavior that does context capturing when started and resumed? Is there any reason you wouldn't do the same for normal function execution? Or is that just too expensive? |
19:25 | <rbuckton> | I expect that's true. |
19:29 | <rbuckton> | Assuming some magical enforcement of syntactic using does support your case, how would you expect it to work? Any alternative I've considered so far breaks some other important feature of resource management. |
19:29 | <Steve Hicks> | Is this in reply to Ron, or the scoped feature in general? I think it’s easily polyfillable. Scope in general - I may just not be seeing it, but short of instrumenting every function to have its own separate mutable context, I don't see how two function bodies sharing the same run context could have Scope mutations from one be isolated from being observed in the other. |
19:30 | <rbuckton> | For example, lets say you could check a function.using meta property from inside the constructor that is only set when you write using x = new Scope(ctx) . That breaks composability, which is another core capability of resource management. |
19:31 | <Justin Ridgewell> | That’s fine, Scope is not composable |
19:31 | <rbuckton> | It prevents a user from wrapping Scope in another function to make decisions. It prevents a user from entering a Scope conditionally. It prevents instrumentation. |
19:32 | <rbuckton> | That would be a huge footgun. |
19:33 | <Justin Ridgewell> | How? We’re jsut trying to allow unnested use of Variable . We don’t need to do anything else. |
19:35 | <Justin Ridgewell> | I was referring to dispose take care of that? And for the dangling async/generator’s mentioned above, it requires a small modification to the pausing behavior. |
19:35 | <rbuckton> | It's a footgun because it's confusing to users. If I had using x = new Scope(ctx) , and wanted to refactor to const createScope = ctx => new Scope(ctx); using x = createScope(ctx); , it suddenly breaks. Nothing else does that in the language, as far as I'm aware. |
19:36 | <rbuckton> | It's a TCP violation, made worse because that specific change did not involve new syntax. the "new syntax" part didn't move. |
19:37 | <Justin Ridgewell> | Perfect, The normal case now is v.run(1, () => …) , which also wouldn’t work that way. |
19:38 | <rbuckton> | take any valid expression in v.run(...) and turn it into an arrow and it would still work. |
19:39 | <rbuckton> | with perhaps the only distinction being this receiver handling, which is a known quantity. |
19:40 | <Justin Ridgewell> |
|
19:40 | <Justin Ridgewell> | These are the same programs |
19:41 | <rbuckton> | So, I stand corrected. One thing in the language has that oddity, and that's how this receivers work. The complexity of this receivers is not something I want to repeat. |
19:42 | <rbuckton> | Where was
then it has the same behavior. |
19:43 | <rbuckton> | If your code was v.run(1, () => { doStuff(); }); , then you didn't refactor it you changed it to a different behavior. |
19:44 | <rbuckton> | I'm talking about changing this
into this
or this
|
19:45 | <rbuckton> | the average JS developer would expect both of those refactorings to be valid. |
19:45 | <Justin Ridgewell> | Ah, I misread const createScope = ctx => new Scope(ctx); , I thought you were doing using new Scope(…) inside the arrow. |
19:45 | <rbuckton> | No. |
19:46 | <Justin Ridgewell> |
That works in my head? using is syntactic, it invokes scope[Symbol.enter]() , function.using check passes, you get a scoped update |
19:47 | <rbuckton> | How is new Scope supposed to know its part of a using ? |
19:47 | <Justin Ridgewell> | function.using ? |
19:47 | <rbuckton> | That doesn't work here. |
19:48 | <rbuckton> | Consider this:
|
19:48 | <rbuckton> | brb |
19:49 | <Justin Ridgewell> | What’s new Scope(otherCtx) supposed to do there? You only enter the returned new Scope(ctx) ? |
19:50 | <rbuckton> | I'm saying that if function.using works when I extract new Scope out into another function, then it works for every new Scope created inside that expression. |
19:50 | <Justin Ridgewell> |
|
19:50 | <Justin Ridgewell> | It’s only checked when entering? |
19:51 | <rbuckton> |
|
19:51 | <rbuckton> | Does it matter? |
19:52 | <rbuckton> |
|
19:53 | <rbuckton> | Either way, that's not what Symbol.enter is for. |
19:54 | <rbuckton> | If we only set function.using when calling [Symbol.enter]() , that means you're going to want to delay resource setup until [Symbol.enter]() is called, which I called out as something we expressly do not want. |
19:55 | <Justin Ridgewell> |
|
19:56 | <Justin Ridgewell> | If we only set Symbol.dispose method. Exactly what’ll happen here. |
19:56 | <rbuckton> | And it still breaks composability. An advanced JS dev might want to subclass or wrap Scope with their own class, and now you've broken them. |
19:57 | <rbuckton> | Yah, it returns a new object with a [Symbol.enter] . |
19:58 | <rbuckton> | IMO, it might be better to have [Symbol.enter] be a property and not a method. |
19:59 | <Justin Ridgewell> | And it still breaks composability. An advanced JS dev might want to subclass or wrap v.run(…, () => {…}) with inline code. Desiging for subclassing or any other power features isn’t the goal. |
20:00 | <rbuckton> | Lets say you have an api like
not
Otherwise people could just call |
20:04 | <rbuckton> | I’m looking to replace nesting of AsyncContext is itself a power feature. The Venn diagram of class of developers that will write code using it, and the class of developers that might have reasons to compose it with other code it is nearly a circle. |
20:06 | <Justin Ridgewell> | Otherwise people could just call [Symbol.enter]() repeatedly on the result of the same openFile call.I don’t see how this example differs from the Scope class above? |
20:07 | <rbuckton> | Because there are thousands of reasons to use a file handle via composition. |
20:09 | <Justin Ridgewell> | That was specfically about the “abuse of the mechanism”, right? I don’t see how your single-prepared FileHandle that can be entered multiple times differs from the Scope class that can be entered multile times? We already hold the ctx and value , we don’t need to initlize it. |
20:09 | <rbuckton> | This discussion has done more to convince me that Symbol.enter shouldn't be a method than it might have convinced me to give it any kind of special privilege like function.using |
20:10 | <rbuckton> | You need to change contexts, which is the actual "resource" you're guarding |
20:11 | <rbuckton> | I assume the essence of what you want is to ensure that developers ship correct programs? |
20:11 | <rbuckton> | (using this feature) |
20:12 | <rbuckton> | And what you want is for the runtime to be able to inform the user when they are using the feature incorrectly. |
20:15 | <rbuckton> | The purpose of [Symbol.enter] is to guide JS users to proper use of the using keyword, while giving sophisticated developers an opt-out mechanism to allow for composability and extend these mechanisms in userland. |
20:16 | <Justin Ridgewell> | I think you’re purposefully containing the feature, which prevents us from using it. |
20:16 | <rbuckton> | If it is absolutely imperative that a context cannot be used in this way, then I would argue that it should only be accessible via run . |
20:17 | <rbuckton> | I'm purposefully not constraining the feature. |
20:17 | <Justin Ridgewell> | We have a valid use case, and you’re telling me we’re abusing the mechanism! |
20:18 | <Justin Ridgewell> | I think you’re going to be sorely disapoointed with userland Symbol.enter functions, and if the proposal doesn’t move forward, get [Symbol.dispose]() will be abused too. |
20:19 | <rbuckton> | Yes, I think its a potential use case, but I'm not sure I'm comfortable with the complexity it adds as it will make composition impossible. |
20:22 | <rbuckton> | Adding a feature like function.using would cause significant problems with adoption. Users would be confused why some resources work with using and DisposableStack , while others don't. It would turn the feature into a minefield. |
20:23 | <rbuckton> | [Symbol.enter] is, at most, compromise. It still allows using and DisposableStack to be used interchangably. |
20:26 | <shu> | i was wondering this during plenary: can TS build [[nodiscard]]? |
20:27 | <rbuckton> | [Symbol.enter] is supposed to be the "staff entry only" mechanism to opt-out. Scope could use that, and maybe even get [Symbol.dispose]() for good measure, and perform implicit cleanup and then throw when the stack of context switches doesn't match what's expected, and that's likely to catch the majority of user errors. |
20:27 | <rbuckton> | i was wondering this during plenary: can TS build [[nodiscard]]? |
20:28 | <rbuckton> | If we have to emit a suboptimal wrapper around every single operation to ensure that it's doing the right thing, then no. |
20:28 | <Andreu Botella> | what about nodiscard as a type error/warning? |
20:28 | <nicolo-ribaudo> | I proposed it to typescript-eslint for using : https://github.com/typescript-eslint/typescript-eslint/issues/8255 |
20:29 | <rbuckton> | what about nodiscard as a type error/warning? |
20:31 | <rbuckton> | You want both using and DisposableStack.prototype.use() to be considered valid use sites. |
20:31 | <rbuckton> | And you want users to be able to build their own DisposableStack subclasses or wrappers that can also declare a parameter as a valid use site. |
20:32 | <rbuckton> | e.g., for polyfills and shims, if nothing else. |
20:33 | <Justin Ridgewell> |
|
20:33 | <rbuckton> | I've given it quite a bit of thought. |
20:34 | <Andreu Botella> | I don't think we need AsyncContext.Scope . For me it's a nice to have, and changing strict enforcement to allow it seems to raise more problems than it solves. |
20:34 | <rbuckton> | I'm not sure you can get the guarantees you want from using without introducing confusion and poisoning adoption. |
20:38 | <rbuckton> | .NET has ExecutionContext , which is essentially the same as AsyncContext . It supports Run , as well as Capture and Restore , flow control, and Dispose , and has for decades. It's unfortunate that AsyncContext has far stricter requirements. |
20:38 | <Justin Ridgewell> | This steps all over the explicit-resource-management proposal, but what if we modify the runtime evaluation of using to handle this internally? Don’t expose a dispose or enter method. |
20:39 | <Justin Ridgewell> | It still breaks symetry with DisposableStack , you can’t wrap it, but it solves the nesting. |
20:39 | <rbuckton> | How? That sounds like breaking composability. |
20:39 | <rbuckton> | If I can't store a disposable in a field on my class so that I can clean it up when my class is disposed, then resource management is useless. |
20:40 | <Justin Ridgewell> | Again, I’m not trying to solve composibility. Your choice is v.run(…) or using _ = Scope(…) |
20:41 | <rbuckton> | Any solution to using _ = new Scope(...) must not break composability in general, and should not poison adoption. |
20:42 | <rbuckton> | "in general" meaning, anything else that isn't new Scope |
20:42 | <Justin Ridgewell> | But the altenative isn’t composable either. |
20:44 | <Justin Ridgewell> | Your class field hypothetical would only be able to expose the new context within the the scope of its methods with this.v.run(…) , same as with using _ = this.scope; |
20:44 | <rbuckton> | are you planning to freeze [Symbol.dispose] and [Symbol.enter] on Scope ? |
20:44 | <Justin Ridgewell> | Neither exist |
20:45 | <Justin Ridgewell> | It’s not a traditional disposable, can’t be use with *Stacks |
20:45 | <Justin Ridgewell> | Can’t manually call the methods to enter or exit. |
20:45 | <Justin Ridgewell> | Can’t leak, only exists within using ’s scope. |
20:46 | <rbuckton> | Your class field hypothetical would only be able to expose the new context within the the scope of its methods with My hypothetical might be something like:
|
20:47 | <Justin Ridgewell> | What would that be with the current AsyncContext’s run() ? |
20:48 | <rbuckton> | Neither exist Scope was never going to have them to begin with, then using definitely doesn't seem like the right fit. |
20:48 | <Justin Ridgewell> | class NamedScope extends Scope {} might work, Scope could hold a [[Dispose]] slot. |
20:50 | <rbuckton> | There is no [[Dispose]] slot in resource management. If what you're proposing requires dramatic changes to resource management for one specific case, it seems more like you need an independent syntax. |
20:51 | <rbuckton> |
or something. What you want to dispose is not a resource, and it doesn't fit within the semantics of |
20:52 | <rbuckton> | or even using scope (ctx) {} , maybe. |
20:52 | <Justin Ridgewell> | It would require an additional branch in AddDisposableResource and 2 additional hint s to differentiate sync/async syntax from sync/async stack.use() |
20:54 | <rbuckton> | You're asking to carve out a narrow corner case for a single type that can't be used the way any other resource could be used. |
20:54 | <Justin Ridgewell> | or even run() , but still requires a nesting layer. Maybe that’s ok. |
20:55 | <Justin Ridgewell> | Yes, I’m willing to privelege language features above userland implemenations. |
20:55 | <Justin Ridgewell> | We do that all the time. |
20:56 | <rbuckton> | If there is to be a special purpose carve-out, i'd much rather there be a syntactic opt-in so users could differentiate between what's safe to refactor to a DisposableStack and what isn't. |
20:57 | <rbuckton> | Yes, I’m willing to privelege language features above userland implemenations. AbortSignal does the same thing and it's one of the worst parts of the design. |
20:58 | <Justin Ridgewell> | I was thinking more Promise fast paths |
20:59 | <rbuckton> | I can understand special cases like skipping an Await for a native Promise but not a thenable. You're already async at that point. But not for a corner case that might cause users to see an entire feature as suspect. |
20:59 | <rbuckton> | I was too. This is not a Promise fast path. |
21:01 | <rbuckton> | Promise fast paths require you to practically go out of your way to observe them and people have been preaching against Zalgo for a decade so people don't take a dependency on ordering. |
21:03 | <rbuckton> | What I don't understand is that if AsyncContext.Scope wasn't going to have a [Symbol.enter] or [Symbol.dispose] to begin with, and planned to special case AddDisposableResource , why would a dangling [Symbol.enter] have even been a concern to begin with? |
21:04 | <Justin Ridgewell> | Yah, this would need to throw if you used it incorrectly. It’s a bit different from fast-path, but I think it’s still a priveleging language features thing. |
21:04 | <Justin Ridgewell> | This is a different approach. |
21:05 | <Justin Ridgewell> | If we can’t get a function.using to detect a public Scope[Symbol.enter] is being invoked with syntatic using , then we can’t expose that functionality. |
21:06 | <Justin Ridgewell> | So what if we didn’t expose anything? |
21:06 | <Justin Ridgewell> | Move it all internal to the language. |
21:08 | <rbuckton> | That still wouldn't address my concern about poisoning adoption. If users expect that using x = res() and stack.use(res()) can be used interchangeably, or that you can use the fairly common "extract to new function" refactoring, then having a single thing that breaks those expectations makes the feature untrustworthy. The only way to address that would be a syntactic opt-in. |
21:09 | <rbuckton> | And with my TypeScript hat on, there's no way we could type Scope in the type system if it depends on a private slot. |
21:10 | <rbuckton> | Well, maybe not no way. No good way. |
21:10 | <Justin Ridgewell> | What would you type? |
21:10 | <Justin Ridgewell> | The syntatic using ? |
21:11 | <rbuckton> | We'd have to have a declaration of class Scope { #_ } and the checker would have to not only verify such a type was reachable, but that it was nominally checked as a result of the private field. |
21:11 | <rbuckton> | Yes |
21:11 | <rbuckton> | It is a type error if the RHS of a using is neither null , undefined , nor Disposable . |
21:11 | <Justin Ridgewell> | Wouldn’t it just be DisposableInterface | Scope ? |
21:12 | <Justin Ridgewell> | And stack.use() would only accept DisposableInterface |
21:12 | <rbuckton> | You need a Scope that isn't comparable via a structural comparison. |
21:12 | <Justin Ridgewell> | Oh, ok. |
21:13 | <rbuckton> | Like, if the definition of
That's essentially just |
21:14 | <Justin Ridgewell> | I understand now |
21:15 | <rbuckton> | We generally don't make such narrow distinctions when it depends on spec internal features. For example, we don't differentiate Symbol.iterator vs Symbol() vs Symbol.for() when used as a WeakMap key as that depends on spec internal information that isn't available via structural comparisons. |
21:18 | <rbuckton> |
using required Disposable | Scope | null | undefined on the RHS and we did the structural comparison to the above, that is reduced to unknown (i.e., unknown and null | undefined | {} are essentially the same) |