Skip to content

Is exnref nullable? #90

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

Closed
lars-t-hansen opened this issue Dec 17, 2019 · 18 comments · Fixed by #97
Closed

Is exnref nullable? #90

lars-t-hansen opened this issue Dec 17, 2019 · 18 comments · Fixed by #97

Comments

@lars-t-hansen
Copy link
Contributor

AnyRef, FuncRef, and NullRef are all nullable: they have NullRef as a subtype and ref.null as a valid value. What about ExnRef? I would be inclined to think not, but I see the overview is quiet on this issue.

Clearly throw will never produce null but what happens with rethrow and br_on_exn, if the exn on the stack is null?

@Horcrux7
Copy link

If ExnRef can be a function parameter or a return value of a function/block then it should be nullable?

@lars-t-hansen
Copy link
Contributor Author

If ExnRef can be a function parameter or a return value of a function/block then it should be nullable?

I'd almost say the opposite, that in those cases it doesn't need to be nullable: for parameters, the non-null value provides an initializing value for the parameter, and for returns it's just a value so there are no concerns about that.

But if we expect to declare locals of type exnref without "let", or globals, or tables, then exnref must be nullable, or there will be no reasonable initialization value, unless we define an "empty" exception value to serve as that.

@Horcrux7
Copy link

and for returns it's just a value so there are no concerns about that.
I have think as a return value if an error occur or null if all is ok. The caller can check if the value is null.

Of course this will also work if I declare the value as anyref and cast to ExnRef if not null. This should also work for global and table values.

Looking in the proposal I see there is no cast possible. I can assign it to a anyref variable but not back. Curious! Then without a nullable option the usage is limit to the local catch block or as function parameter.

This will also ok for me because I use it only as boxing for a anyref value which contains my real exception currently. The disadvantages is that with the unboxing a stacktrace is lost.

Conclusion: A nullable will give more options for the compiler writers. But I think the embedder must decide which impact this has. For a MVP a non nullable seems ok.

@aheejin
Copy link
Member

aheejin commented Dec 18, 2019

It is nullable, otherwise we don't have any ways to initialize locals. And also, it is castable to anyref (we don't have a cast operator yet, but a block or function whose signature is (something)->(anyref) can return a exnref), so if exnref is not nullable while anyref is nullable does not make sense. I guess I should make this clear in the overview.

I haven't thought about what to do on when br_on_exn takes a nullref. Maybe we should trap?

@lars-t-hansen
Copy link
Contributor Author

It is nullable, otherwise we don't have any ways to initialize locals. And also, it is castable to anyref (we don't have a cast operator yet, but a block or function whose signature is (something)->(anyref) can return a exnref), so if exnref is not nullable while anyref is nullable does not make sense. I guess I should make this clear in the overview.

I haven't thought about what to do on when br_on_exn takes a nullref. Maybe we should trap?

If exnref <: anyref then upcasting just happens, as for all the other ref types, so that's not an issue. And it's fine for exnref to be non-nullable even if anyref is nullable, I don't understand what you mean by that not making sense - anyref can hold a strictly larger value set than exnref, and an upcast exnref -> anyref will be unproblematic. A downcast anyref -> exnref (with a cast operator, which we do not have, in the same way we do not have it for funcref or nullref) would need to check against null, if exnref were non-nullable, then against the type of the value.

IMO, with nullable exnref, both rethrow and br_on_exn should trap if the operand is null; we may need exnref to be nullable, but I think we should contain the damage as much as possible. If this turns out to be a hardship we can loosen the restriction later.

Having thought about it slightly longer, I probably favor nullable exnref since exnref is a "fallback" type a la funcref; it needs to accomodate as many situations as possible.

@aheejin
Copy link
Member

aheejin commented Dec 18, 2019

If exnref <: anyref then upcasting just happens, as for all the other ref types, so that's not an issue. And it's fine for exnref to be non-nullable even if anyref is nullable, I don't understand what you mean by that not making sense - anyref can hold a strictly larger value set than exnref, and an upcast exnref -> anyref will be unproblematic. A downcast anyref -> exnref (with a cast operator, which we do not have, in the same way we do not have it for funcref or nullref) would need to check against null, if exnref were non-nullable, then against the type of the value.

You're right, upcasting is not an issue. But we need it to be nullable for locals anyway.

IMO, with nullable exnref, both rethrow and br_on_exn should trap if the operand is null; we may need exnref to be nullable, but I think we should contain the damage as much as possible. If this turns out to be a hardship we can loosen the restriction later.

Come to think of it, we may not need to trap after all. I don't see why throwing and rethrowing nullref is a problem; it makes sense when we are not gonna look at the thrown values and only intend to transfer control flow. And for br_on_exn, it's also not a problem because nullref does not have a tag, so br_on_exn is never gonna match on it.

@lars-t-hansen
Copy link
Contributor Author

IMO, with nullable exnref, both rethrow and br_on_exn should trap if the operand is null; we may need exnref to be nullable, but I think we should contain the damage as much as possible. If this turns out to be a hardship we can loosen the restriction later.

Come to think of it, we may not need to trap after all. I don't see why throwing and rethrowing nullref is a problem; it makes sense when we are not gonna look at the thrown values and only intend to transfer control flow. And for br_on_exn, it's also not a problem because nullref does not have a tag, so br_on_exn is never gonna match on it.

It's not a "problem", because we can always perform a null check in br_on_exn (you can't match the tag until you know the value is not null). It just seems pointless to allow throwing null when we don't have a use case for it, and especially when we can change our minds about this later if a use case appears. If rethrow is restricted from throwing null then a br_on_exn in a catch block where the exnref is known to have been thrown (ie we've not loaded it from some variable where we stored it earlier) does not have to check for null. It's not a big deal, but it just seems cleaner to me.

I also think that if rethrow is allowed to throw null, and we therefore have to have a null check in br_on_exn, then we should trap there on null and not just treat a null value as a failed tag match. This relates to what I wrote earlier about "containing the damage". Discouraging null values from being considered valid is a good idea in general.

@aheejin
Copy link
Member

aheejin commented Dec 28, 2019

It's not a "problem", because we can always perform a null check in br_on_exn (you can't match the tag until you know the value is not null). It just seems pointless to allow throwing null when we don't have a use case for it, and especially when we can change our minds about this later if a use case appears. If rethrow is restricted from throwing null then a br_on_exn in a catch block where the exnref is known to have been thrown (ie we've not loaded it from some variable where we stored it earlier) does not have to check for null. It's not a big deal, but it just seems cleaner to me.

I also think that if rethrow is allowed to throw null, and we therefore have to have a null check in br_on_exn, then we should trap there on null and not just treat a null value as a failed tag match. This relates to what I wrote earlier about "containing the damage". Discouraging null values from being considered valid is a good idea in general.

I don't fully understand why throwing nullref should be considered as 'damage'. You can throw nullref to just cause stack unwinding and transfer the control flow to somewhere in one of the callers. I'm not sure why we should restrict the spec in terms of what it can do..?

and especially when we can change our minds about this later if a use case appears.

I think this is true the other way around too :)

@rossberg
Copy link
Member

rossberg commented Jan 7, 2020

IT would be cleaner if we didn't have nullability leak into exnref. On the other hand, if we want to avoid proposal dependencies, we could have both, and have this proposal start with nullable exnref, but later add non-null exnref, which would be a subtype. That would fit with the slight refactoring that introduces opt/non-opt ref types as part of the typed func ref proposal.

@lars-t-hansen, in what sense is exnref a "fallback" type? The funcref type is a bit different, since its only use case is call_indirect, which involves a runtime type check anyway. That isn't true for exnref.

@dschuff
Copy link
Member

dschuff commented Jan 8, 2020

It may be cleaner, but let's not forget that a major practical reason we switched to using reference types for exceptions in the first place is that the exception package needs to be able to escape the catch block. If exnref isn't nullable and there's no way other than a catch to initialize one, then that entire purpose is defeated. (In other words, even if we wait for non-nullable types and let bindings, it won't be useful for exceptions since there's not really a meaningful value for an exception package other than null or something thrown).

I don't have strong feelings about allowing rethrow or br_on_exn of a nullref.
@aheejin you can't actually directly throw a nullref; there's no way to specify the exnref value, the instruction only specfies the type (and the contained value if applicable), but the exception package itself is always created by the runtime. Because of that it maybe makes sense to trap on rethrow too; although it would be an extra null check in that case.

@rossberg
Copy link
Member

rossberg commented Jan 8, 2020

If we allow null into exnref then trapping on rethrow is the only sensible choice. For example, in a typical engine, null wouldn't have any stack trace attached to it that rethrow may want to access and extend. Also, on the receiving side, there would be no way to distinguish null from an exception package that caught a thrown null, although these are quite different things.

@aheejin
Copy link
Member

aheejin commented Jan 8, 2020

@dschuff
I thought being able to throw nullref would have some use cases such as transferring the control flow without sending values, but being able to only rethrow nullref while we can't actually initially throw nullref does not make too much sense, maybe.

@rossberg

there would be no way to distinguish null from an exception package that caught a thrown null

I'm not sure if I understand what 'null from an exception package' mean?

@backes
Would that be a problem in the engine implementation if we decide to trap on rethrow and br_on_exn if the argument is nullref?

@rossberg
Copy link
Member

rossberg commented Jan 8, 2020

@aheejin:

I thought being able to throw nullref would have some use cases such as transferring the control flow without sending values

For that you can define an exception with no arguments.

there would be no way to distinguish null from an exception package that caught a thrown null

I'm not sure if I understand what 'null from an exception package' mean?

What exnref value would you get when you catch null? Would it be distinguishable from null itself? Consider:

(local $e exnref)  ;; null
(try ... catch (global.set $e) ...(A)...)
(if (ref.is_null (global.get $e)) ...(B)...)

When control reaches (B), does that mean that (A) was never reached, or could it be the case that somebody threw null? (And in the latter case, is there a stack trace?) The semantics are muddied if you cannot distinguish these cases. But how could you distinguish them when you essentially made null a proper exception itself?

@rossberg
Copy link
Member

rossberg commented Jan 8, 2020

FWIW, in general, there is no problem with initialisation of non-nullable exnrefs. It's trivial to define a dummy exception and use that as a null value where needed. The only problem is locals, which currently require an implicit default initialiser. That's why you'd want let. (In retrospect, it was shortsighted design to not have init exprs for locals.)

@aheejin
Copy link
Member

aheejin commented Jan 8, 2020

@rossberg

@aheejin:

I thought being able to throw nullref would have some use cases such as transferring the control flow without sending values

For that you can define an exception with no arguments.

That makes sense.

there would be no way to distinguish null from an exception package that caught a thrown null

I'm not sure if I understand what 'null from an exception package' mean?

What exnref value would you get when you catch null? Would it be distinguishable from null itself?

I don't understand this part. Are there multiple nullrefs? If nullref is thrown, nullref is caught. Not sure what you mean by 'null itself'.

Consider:

(local $e exnref)  ;; null
(try ... catch (global.set $e) ...(A)...)
(if (ref.is_null (global.get $e)) ...(B)...)

When control reaches (B), does that mean that (A) was never reached, or could it be the case that somebody threw null? (And in the latter case, is there a stack trace?) The semantics are muddied if you cannot distinguish these cases. But how could you distinguish them when you essentially made null a proper exception itself?

In this case, if we allow nullref to be rethrown, we cannot distinguish whether (A) was never reached or somebody threw nullref. I'm not very sure why distinguishing this is important to exception semantics.

Just to be clear, I'm not against trapping on nullref for rethrow and br_on_exn at this point. I agree that throwing an empty exception will achieve the same effect and allowing nullref might be more confusing than helpful.

@aheejin
Copy link
Member

aheejin commented Jan 8, 2020

@rossberg

FWIW, in general, there is no problem with initialisation of non-nullable exnrefs. It's trivial to define a dummy exception and use that as a null value where needed. The only problem is locals, which currently require an implicit default initialiser. That's why you'd want let. (In retrospect, it was shortsighted design to not have init exprs for locals.)

As @dschuff said, the whole purpose of introducing the current proposal was to allow exnrefs to escape catch scope, and it wouldn't be easy to define a scope for each exnref other than the whole function. If one day we have to use non-nullable exnref, and if we are given an instruction that generates a dummy exnref or something, we'd be likely to use it like current locals - initialize it with dummies in the beginning of a function and make the let cover the whole function.

But anyway, I would not like to make the EH proposal also dependent or blocked on the let construct and the function reference proposal for now. We can add it afterwards maybe. (I'm still not sure about its usability, and I think we should still keep the original nullable exnref type in that case.)

@rossberg
Copy link
Member

rossberg commented Jan 8, 2020

I'm not very sure why distinguishing this is important to exception semantics.

Simply because they are completely different things. One is an absent exception package, the other a present exception package that contains an empty exception value. The latter should e.g. be associated with a stack trace.

A code pattern like the above would very likely be buggy code if null could be thrown. And you'd have to jump through additional hoops (maintaining an extra flag) to fix it. Most folks would likely be tempted to avoid the hassle and rather introduce the latent bug hoping that nobody else throws null.

(This is the usual composition problem introduced by abusing the sentinel null for optional values on multiple levels, where you cannot distinguish an empty option from a non-empty option containing another empty option. It's the same conflation created by e.g. a dictionary whose lookup function returns null when a key is not present, but also allows null as a value. It's what e.g. led JS to introduce both null and undefined, which of course just kicks the can down the road, because undefined can also be used as a value.)

aheejin added a commit to aheejin/exception-handling that referenced this issue Jan 8, 2020
Following the discussions in WebAssembly#90, it seems desirable and less
error-prone to make `rethrow` and `br_on_exn` trap in case the value on
top of the stack is of `nullref` type.
@backes
Copy link
Member

backes commented Jan 9, 2020

@backes
Would that be a problem in the engine implementation if we decide to trap on rethrow and br_on_exn if the argument is nullref?

Not at all, that's easily doable.

aheejin added a commit that referenced this issue Jan 10, 2020
Following the discussions in #90, it seems desirable and less
error-prone to make `rethrow` and `br_on_exn` trap in case the value on
top of the stack is of `nullref` type.

Closes #90.
backes added a commit that referenced this issue Mar 23, 2020
In #90 it was decided to move the event section between memory section and global section. This change is reflected in the next paragraph, but not in the introduction.
backes added a commit that referenced this issue Mar 23, 2020
In #90 it was decided to move the event section between memory section and global section. This change is reflected in the next paragraph, but not in the introduction.
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Jun 6, 2020
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Jun 6, 2020
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Jun 6, 2020
Following the discussions in WebAssembly#90, it seems desirable and less
error-prone to make `rethrow` and `br_on_exn` trap in case the value on
top of the stack is of `nullref` type.

Closes WebAssembly#90.
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Jun 6, 2020
In WebAssembly#90 it was decided to move the event section between memory section and global section. This change is reflected in the next paragraph, but not in the introduction.
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Feb 23, 2021
Following the discussions in WebAssembly#90, it seems desirable and less
error-prone to make `rethrow` and `br_on_exn` trap in case the value on
top of the stack is of `nullref` type.

Closes WebAssembly#90.
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Feb 23, 2021
In WebAssembly#90 it was decided to move the event section between memory section and global section. This change is reflected in the next paragraph, but not in the introduction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants