Make the branch type checking consistent with the type system.#215
Conversation
|
You might want to check out issue #179 and the corresponding PR #180. ;) As for multiple values, you can go back to an old version of this repo (pre #53) to see how they could work. That implementation allowed multiple arguments to break, symmetric to multiple arguments to call or return (the latter in fact being desugared to multi-arg break). AFAICT, that was the most consistent design. |
|
@rossberg-chromium This reverts the change in #179 It creates an inconsistency in the type system to accept Please read above 'The misunderstanding appears to be an interpretation of the optional argument(s) as a list of the values ...' I did see the prior multi-value support for What I think was wrong with that was that 'The misunderstanding appears to be an interpretation of the optional argument(s) as a list of the values ...' Can we fix this? |
|
Here's another example to consider. If If you want an operator to concatenate all the values then add it separately so it can be used in other contexts too. The It is also inconsistent to be able to return multiple values built from a list only from a rather than |
|
@JSStats, it is instructive to consider the analogy with calls. When you have a function without arguments, you call it with a In the multi-value extension, break allowed arbitrary arities, just like calls. And just like calls, it allowed a syntactic list of values. In addition, it also allowed a single argument that has a multi-value type like you suggest -- and so did call, in a completely consistent manner. That is, the requirement for matching arity syntactically was removed everywhere. However, that is an extension that does not make much sense without consistent support for multi-values, IMO. I don't think it is worth introducing it as a special case just for breaks. As a corner case, it can be more confusing than useful, see #179. |
Again, the question would apply to calls as well. The answer lies in the Effectively, the multi-argument forms of call, break, and return are sugar It is also inconsistent to be able to return multiple values built from a
|
|
@rossberg-chromium I think there is a clear distinction between arguments and results. Consider how the type system handles results now - if no values are expected it still allows the expression to return values, it does not demand that the 'arity' of the expected number of values matches the number of values the expression produces, well there is no defaulting so it demands at least as many values as expected, but excess values are discarded. My concern is that multi-value support has not been thought through. Could I ask if you have ever used a programming language with multi-value expressions? If so could I have a look how it worked, to better understand your expectations. You still have not answered what are the result values from break if given multiple expressions that return multiple values under you model? With just one it returns all the values from it, but what about two or three each with a different number of values? Think it through, perhaps you'll start to see the problems, how complex it has become, and that if you want these operators it is best to separate them from break and keep break simple accepting a single expression with multiple values. Even under your model it is not consistent, if break with multiple arguments builds a multi-value result expression then it is still only necessary that the blocks expected type have the same number or less values. E.g. |
|
@rossberg-chromium |
|
@JSStats, no, in the multi-value extension, I thought I answered your other question already. You cannot "merge" multi-values, it simply doesn't type-check. When forming a multi-value, each member has to be a plain value. The multi-value support has been thought through very thoroughly, I can assure you. All your questions about what multi-break means are hopefully answered by pointing out that you could simply desugar if you made Once you have multi-return, there is absolutely no conceptual difference between multiple arguments and multiple results. The only reason that exists in many languages is that multi-return often was an afterthought. |
But this currently fails to validate! We do have multi-values, we have
This is not consistent with the multi-value support you suggest, in the case in which the break expression is a single function call returning multiple values you suggest it returns them all, yet above you define 'each member has to be a plain value.' and for it to be invalid to pass multiple values.
Good one, can I quote you on that?
Wrong,
Could you please direct me to a programming language consistent with what you are proposing? Or is it that all programming languages are legacy and you just happen to be 'doing it the right way' because they all got it wrong? You mention 'unification' a lot, perhaps you see multi-value as a structural matter, but the AST is flat (basically) with each operator standing on it's own with each argument and result having a type including multi-values. Sorry if I am reaching here, but I really am trying to understand your perspective. The function arguments case is a destructing operation as opposed to a construction operation. For example, what does Recall you defined it above as 'each member has to be a plain value.' and I quote 'thought through very thoroughly, I can assure you.' ;) Programmers are pragmatic, sometimes they want to concatenate values, to build up an argument list, and sometimes they just want the first value. I don't think needing both operations is a sign that it was an 'afterthought', nor is separating destructing from construction. If nothing else their use shows that the patterns were useful. |
We do have multi-values, we have nop returning zero values, and single
That's not general multi-values, though. Look, I'm not saying what you I thought I answered your other question already. You cannot "merge"
The multi-value support has been thought through very thoroughly, I can
No, I think you are confused. Which is probably my fault. Let me try once
Everything else follows from these. Could you please direct me to a programming language consistent with what
You mention 'unification' a lot, perhaps you see multi-value as a
The function arguments case is a destructing operation as opposed to a
For example, what does (call (values )) pass to the
|
|
@rossberg-chromium We seem to both agree that |
If we could please just change the MVP type system so that this validates
|
|
FWIW, I also maintain that this does _not_ make the type system more
consistent. Sans multi-values, the type system currently consistently
requires syntactic arity matching everywhere. This change would introduce
an exception.
|
|
@rossberg-chromium I'm getting a little confused here on your position, you seem to be flipping. You had been maintaining that '(module (func (block $1 (br $1 (i32.const 1)))))' and '(module (func (block $1 (br $1 (nop)))))'would validate with your view of multi-value support, but just above you claim it would not be consistent. Which one is it now? Flagging a pattern as invalid now, when it is expected to be valid in future, might sound like a precautionary path. However, it is not, because the encoding will be optimized for valid code, and we could very well end up with an encoding that can not even represent the expected valid code, and with implementations that do not support it. The best path to ensuring there is a clear path forward is to reserved this pattern by making it validate as expected now and to add tests for this. Thus it is important to work through this. Let me work through your language above:
If However I believe the consumers of these should be matching them with Only after this pattern matching, would the bound arguments be type checked, the rest discarded and not checked. btw: The empty type list
Here Could you please give an example for The
You would need to define 'has type'. SML might have different definitions of types here than are useful in this context. I have tried to define the rules above using SML pattern matching. For example a consumer of one value matches with |
|
I would like to add the encoding of the For an example of consequences of not addressing this pre-MVP I would point to the v8 encoding of FAQs:
|
|
On 29 January 2016 at 13:20, JSStats notifications@github.com wrote:
|
|
SML/Ocaml is a high level language with a pattern matching focus. WASM is a low level language with no pattern matching, and close to the hardware. The 'arity' constraints that have been applied are not even constraints in Ocaml - they just need explicit workarounds. Below are some examples in Ocaml of discarding excessive values, and consuming only part of the values to prove this point. For the compilation-target use case it would be a burden not to be able to lower such code into efficient wasm (ease for the producer, encoding wise, and performance wise). Frustrating wasm code that wants wants to emit these pattens in completely unnecessary. The wasm runtime compiler always knows the number of values expected and the number of values actually available and it is an almost trivial matter to discard them. The 'arity' check between the first values of the I am a developer and maintainer of a language with support for multiple value expressions - and with a type system for reasoning about these types. I would like to be able to lower to efficient wasm code when the number of values is known. It not clear how addressing this could negatively impact code compactness so if you could clarify you concern that the restrictions are 'needed for code compactness, especially in the common case of calls' then I might be able to address this too. As can be seen in the workarounds necessary in Ocaml below, the restrictions would bloat the AST, it would be necessary to do the same in wasm, to destructure all the actual values into local registers and rebuild expression results. Unless there is an efficient way to transform such code patterns into the wasm AST then I can't give up on this issue. |
|
On 31 January 2016 at 01:12, JSStats notifications@github.com wrote:
Performance is not affected by this either way.
There is no other motivation than keeping the language simple. If in doubt, I am a developer and maintainer of a language with support for multiple
Unless there is an efficient way to transform such code patterns into the
;; Example of consuming only part of result values which can be done,
;; Example of consuming only part of three return values, but this
|
Even if it only bloated the AST this would be a very significant issue. It will affect performance if there is a lot of unnecessary destructuring into local variables to receive all the values when only one or the first few are used, and if the runtime compiler is not smart enough to optimize these away.
Discarding unused values is simple for the runtime compiler. Common Lisp and Lua have conventions that consume only the used values and discard the rest, this is the convention for every consumed expression result except for those consumed by special operators that consume multiple values. For example an i32.add could accept multiple value expressions for each argument and it consumes only the first. The destructuring also has this convention - excess values are ignored. They also default unsupplied values but I don't think wasm should be burdened with that. It's a trivial matter for wasm to discard these excess values and it avoids a lot of redundant destructuring. For example it allows: Rather than That's a big savings.
If you could give an example of 'short forms with multiple arguments' then I might be able to address this. The only benefit that comes to mind is an encoding optimization using the context to reduce the number of arguments to Could we accommodate these encoding optimizations in other ways, at a lower layer than the AST? Alternatively could we simply have separate opcodes, some that depend on context and some that do not. To be clear: to add Add a separate operator for calling a function with the concatenation of all the values from the arguments, rather than taking one value from each argument as usual. E.g.
I though it would be an arity mismatch to discard values, even all of them, or was it just an arity mismatch to discard some of them. I am not arguing that this could not be done, just that it would have require destructing to discard them? Perhaps you could give an example of how efficiently this could be expressed?
It is not the case in CL and Lua that Again, it is a trivial matter to implement this in wasm. I strongly dispute that it is 'a non-trivial subtyping extension to Wasm just to make it slightly more convenient than the others.' I am prepared to prove this, but we don't have any mv implementations yet to modify. I could certainly demonstrate it with AST validation code for the proposed mv support.
The list expresses the pattern of matching the head of the list and discarding the rest. It's a weakness of the SML compiler if it does not optimize this case, just as it would be a weakness of an SML compiler to box tuples above. With wasm discarding unused trailing values it is possible to lower the above into very efficient wasm, efficient to represent in the AST. |
The difference is tiny, the case is very rare, and I doubt that it would
Whow, that's even more pervasive subtyping than I thought you'd be asking
The ones we were talking about: The only benefit that comes to mind is an encoding optimization using the
Again, this seems like far too much featurism for a low-level language. ;; Example of discarding unused values.
All the examples use multiple values. In the proposed multi-value ;; Example of consuming only part of result values which can be done,
Note that even if Wasm had this, it would help exactly zero with ;; Example of consuming only part of three return values, but this
|
Perhaps this is a core misunderstanding of the importance of not being unnecessarily restrictive here. For you edification, below function This could be lowered basically into wasm: |
|
Fair enough, but does not change my overall assessment. Wasm is a low-level language. There can't be any expectation that it can represent every convenience feature from a chosen high-level language 1-to-1. |
|
Here is another example of a real test that was needed. It is significantly convoluted due to the demanded arity[sic] checks. Realized it could be simplified a little while writing this up, see below. Without the arity checks this could have been as simple as: It could also be reworked to the following to work around the arity checks, but I don't believe producers should have this burden of searching for a work around which might have more constraints than in this example. These checks seem to serve no purpose, they frustrate code production and the workarounds lead to convoluted code. If the producer wants to discard values it should be easy, and it should not differ between a block fall-through expression and a break expression. There also seems to be agreement that these checks would not be applicable in future with mv support (although perhaps some flipping on this). Flagging them as invalid now seems to be leading to the encoding optimizing away the capability to express what are expected to be valid expressions in future, a very unfortunate outcome. If there is a desire to optimize the encoding based on the expected number of values in some contexts then it should be a simple encoding decision and not demand re-organization of the AST, for example separate opcodes, and can we take another look at that matter later when optimizing the encoding and fix the AST now please. This patch will need a little reworking after the |
|
Consider a (pre- or post-order) decoder that encounters the |
|
@lukewagner The pre-order decoder has nothing to decide, it knows there is an expression and decodes it, just as for any other fixed argument opcode. For a post-order decoder a expression is popped. This matter is resolved. There is no arity[sic] issue here. It's just an operator with a fixed number of arguments. Yes, it would be a fixed argument operator, just as in the v8 encoding for br. This is not something foreign to wasm, there are lots of fixed argument operators. The problem is that the semantics of the br expression differ from the fall through. This demands non-trivial re-organization of the AST to work around. This is the matter to be resolved, and it can not be settled by looking at the encoding efficiency alone - it complicates a producer. If you want to optimize the number of arguments of operators based on their context then this could be done much more cleanly by introducting new operator variations. These could be substituted in a later encoding pass without restructuring the AST, and only in places where they fit without demanding a restructure of the AST. This would only be justified if it had an impact, and it might not even be justified, and we might just have clean AST operators. You have also missed the point that with mv support, and with either proposal, that the very invalid code that supports the optimization you argue for will be valid, so this optimization will not be generally applicable for these same operators in future - it's a dead end. I would like the AST to be clean now. I would accept it as a resolution to keep the context dependant operators, which could be an encoding option, if clean fixed argument count operators are also available and that these do not have these artificial arity[sic] restrictions. These would remain a fixed argument operator even with mv support and continue to have the same semantics as the fall-through expression result. |
|
Perhaps we could take a break from this discussion, although I'm happy to answer any questions. I'll try to implement something concrete, a patch for v8 adding support for both context specific and fixed argument count break operators. The v8 encoding already uses a nop for fill, so context specific operators might help address the encoding efficiency use case, while the current nop-fill variant could handle the general cases. V8 already has a context specific return operator, so already uses some top-down context. So rather than a change request this would become more of an enhancement. |
|
I see; I didn't know the v8 encoding always wasted a byte by synthesizing Anyhow, after writing my last comment, I realized that a postorder encoding wouldn't have context available (duh; late night) and so a postorder encoding would also need two separate opcodes to distinguish nullary vs. unary. With the opcode table design, each opcode needs to be defined by some operator name string which does then imply separate I know post-order isn't decided (although @MikeHolman and the other Chakra guys were reaffirming their preference recently and I'm also now in favor), but so far this is the only type system issue that I know of that would get in the way; having arity depend on parent context seems to be a peculiar property of @rossberg-chromium How about it? |
|
Let me try another appeal that might now be compelling. There seems to be serious consideration of moving to a largely post-order encoding, and still a goal of single pass SSA conversion and validation. However a one pass validator could not know the consumed type of a block when break operations within the block scope to the block label are decoded and this seems to fundamentally block single pass validation with the current Here is an example that currently fails validation with an arity error in the spec ml-proto, but would appear to be impossible to catch in a single pass post-order decoder validator. This can be seem in the proposed v8 post-order implementation https://codereview.chromium.org/1830663002/ which at the end of a block discards the results of all but the last expression by simply adjusting the AST node stack. It does not make another pass over the decoded expressions to detect these 'arity' conflicts, and doing so would be an added burden. So I make an extra appeal on this issue from a decoder efficiency perspective. |
|
One alternative to the methods mentioned here is that we give type to blocks. And then it is required that all brs targeting the block produce the correct type. This allows decoders to do any desired up-front reservation (e.g. for us, we would want to on entering a block reserve a tmp register which all brs would store to), and it allows for a simple lookup to know what to consume which we already do to find label we are branching to. This would be similar in nature to how function calls behave -- we know what arguments to read by doing a lookup on the type of the function we are calling. |
|
@JSStats I think it's generally agreed at this point that the validation rules are going to need to change to support single-pass, bottom-up postorder validation. I think the concrete proposed changes are forthcoming, but, fwiw, in our current bottom-up algo in SM, when you enter a block, we push a block on a block stack that contains an out-of-band (where |
|
@lukewagner Thank you. The SM rule seems to validate this: The following also appears to validate This rule seems to basically assert that where result branches have different types they must not be consumed, and it delays this check until they are consumed to support the post-order single-pass validation? Such a rule would appear to scale very nicely to multiple values were only a subset are consumed, but it would be good to extrapolate this to multiple values to understand this too. Fwiw (probably bike-shedding) in CL a distinction is made between expected types and actual types because their validity is defined by a set relationship - the actual type must be a subset of the expected type. So the SM expected |
|
Correct on those examples validating in SM (specifically after bug 1254142 lands). I should be clear that this is in anticipation of upcoming bottom-up/post-order validation changes (we were forced to flip from top-down to bottom-up for the current binary encoding design which has no FWIW, my interpretation of our |
|
On Tue, Mar 29, 2016 at 5:53 PM, Luke Wagner notifications@github.com
|
|
@titzer We don't need arities because the number of values for multiple values expressions do not need to match rather it is only necessary that consumed values match. This can already be seen for the case of zero-values and one-value expressions, in |
|
If I understand Luke's lattice proposal, Unify of mismatched arities will On Tue, Mar 29, 2016 at 6:37 PM, JSStats notifications@github.com wrote:
|
|
@titzer Well, bottom (i.e., |
|
@titzer But the same model could be extrapolated to multiple values in a different manner so that values that are the same type could be consumed while still failing on an attempt to consume a union of values of different types. So this seems an issue with the model not a constraint on wasm. Here is a model I propose and some examples. Although defined by bottom-up procedures below I believe it is equivalent to set theory relationships so applies bottom-up or top-down. I believe I could demonstrate single pass SSA translation and validation with this model, where multiple value expression results are transformed into multiple SSA definitions one for each expression value and only the consumed values are validated and attempts to merge definitions with conflicting types would be dropped as they are either not used or result in a validation failure that would be caught by the propagated node type. At the end of single pass SSA translation the multiple value expressions would be all gone perhaps except for some annotations on nodes that are sources of them such as calls and at function exits where they are constructed. It would be good to understand potential problems people see with this model to be able to contrast it with other proposals? I understand supporting multiple value expressions is not necessary, that wasm need not even support blocks returning single values or even expressions at all. My arguments will be: that it's not too large a burden and I may well be able to demonstrate this in an implementation; and that it can lead to more compact wasm code compared with shuffling everything through local variables. Even if wasm had no expression support at all and all operators wrote results to local variables, I would still like to see support for returning multiple values from a function and receiving them into multiple local variables in the callee because it could support returning these in hardware registers offering improved performance. |
…cking consistent with the fall-through expression. The prior code make the number of arguments to break and return operators dependent on the number of expected values, however this property does not hold in general. For example consider a single break with a single expression that returns the type (), the same type as `nop`, here the number of expected values could be zero yet there is one argument. Further the prior code appeared to make an interpretation of the arguments as a tuple constructor that would increase with multiple-value support, yet this is not consistent with the fall-through which is a single expression. The function return operator has the same issue. Even though the function expected results might be considered side-information, the number of arguments to return does not in general match the number of expected function results. Wasm currently has the start of multiple value expressions, there are single value results and the empty-values result from nop, so even now this needs some consideration to be consistent. There is some interest in exploring serializations of the AST that are not dependent on the context, and thus a need to have each operator have a defined number of arguments, not necessarily fixed but if not fixed then encoding in the operator. There is also a strong interest in having an efficient encoding for the zero argument break and return operators, even if this means exposing this at the AST level. The solution here adds separate zero expression break and return operators, which have a fixed number of arguments. These are only valid when the expected number of values is zero, but this does not add a new constraint and is still consistent with the fall-through expressions. The current break and return operators become fixed argument single expression operators for which their expression has the same semantics as the fall-through. Future multi-value expression support would be expected to add an explicit tuple or values constructor, matters yet to be decided, and these would likely be usable for the fall-through and the break or return expression so no new break or return expressions would be needed.
|
@lukewagner, I don't think the spec needs to (or should) change for post-order. With type systems, you usually have two distinct concerns: a declarative specification of typing rules, and an algorithmic implementation (or many possible ones). The former is completely unaffected by the move to a post-order encoding. Implementations merely want a different algorithm for checking now. For a spec, however, you ultimately want to use the usual declarative formulation, and the current top-down formulation happens to be in almost 1:1 correspondence to that (in particular, because it does not require type variables or unification). It might be useful, however, to add a bottom-up algorithm to the spec as a kind of "implementation note". In fact, that's already on my list of things to look into. But I don't think this algorithm should become the actual spec. |
|
@rossberg-chromium If the |
|
I think Andreas is arguing that bottom up and top down should still be On Mon, Apr 4, 2016 at 5:32 PM, Luke Wagner notifications@github.com
|
|
I understand that, but I'm asking if implementing the precise validation rules in ml-proto will cause undue hardship for a post-order/bottom-up impl. E.g., I think V8 does not precisely implement these rules and, e.g., accepts the above example, right? Are you excited to make that fail to validate :) ? |
|
@lukewagner, yes, I'm aware of the problem, but that's an algorithmic problem entirely, it does not affect the definition of the type system. Declaratively, typing rules have no direction -- they don't check one type against another, they just require certain types to be in suitable relations. A type system's specification does not need to say how to actually come up with the types in this relation (as long as we can show that there is an algorithm that can, and we can give several in this case). Since you mentioned Hindley/Milner, note the similar difference between the H/M type system and the algorithm W for type checking it. The former is much simpler than the latter, and needs no notion of unification or anything like that. |
|
(Oops, sent before I saw your latest comment.) |
|
Yes, sorry if I was unclear: I'm asking if the definition needs to change to admit a simpler algorithm. |
|
@lukewagner, the example is one we've already been discussing independent of post-order, so I would argue it's mostly orthogonal. But you are right that the ease of a post-order algorithm may be another argument for allowing it. |
|
mostly :) |
|
I just noticed that a cool pre-order AST single pass compiler to a stack machine byte code, and an interpreter for this byte code, landed on sexpr-wast. I am interest in adapting this to try and demonstrate some different designs for multiple-value support and in the limit for zero and one value expressions for the MVP. The compiler does not appear to exploit the known expected type of blocks, and could I ask if this is a deliberate design decision and perhaps in anticipation of a post-order AST compiler? One approach I could explore is to determine the expected type for blocks when they are created (at the shift operation). The expected type appears to be available with the pre-order decoder by looking at the top of the expression stack. Then the breaks could just compare to this without the 'unify' logic. I could scale this to multiple values by having the break operators compile in discard/keep operators that discard excess values and keep the expected number of values. Unfortunately I don't think this would work with a single pass compiler of a post-order AST as it would not know the expected type of a block before the break operators. It's not immediately clear if this compiler would work single-pass with a post-order AST encoding? The other approach that comes to mind is to use the type (number of values) of the first block break as the actual result type of the block. If other break operators passed more values then the excess would be discarded, and if less then the compiler might need to emit fill values and validate that they are never used. If break operators had conflicting value types for some of the values then these conflicts could be ignored too as there would either never be used or cause a validation error. |
|
@JSStats stack machine? interesting. Can you give a link? |
You know, I did experiment with this, but I can't remember why I moved away from it. I just looked at the diff in my reflog, but I still can't remember why. :-} I definitely ran into issues type-checking top-down, especially making this work with the expected value stack -- i.e. what values if any should be discarded. But then again there were a number of issues with the code at that point, so it doesn't mean that it couldn't have worked. |
|
My current thinking is that blocks should not return values, rather use local variables for the use case. |
* Fix string comparison Classic newbie mistake of using != on strings. Plus I got the conditional wrong - it should error if s is none of the valid simd shapes.
Fixes WebAssembly#215. This should indicate that an engine is still conforming if it does not attach a stack trace, even if `traceStack` is requested by JavaScript code (by setting it to `true`).
The branch optional argument should default to the empty-values type None, as from a nop operation. To be consistent with the type system a branch should validate when the expression is not included or the expression returns the empty-values type None and when the target expected type is also None.
When the optional expression is included and returns a single-value type such as i32 the branch should also validate when the expected type is None. This is constent with the type system which accepts more values than expected, for example accepting a single value type such as i32 when expecting type None.
The misunderstanding appears to be an interpretation of the optional argument(s) as a list of the values to return, but multi-value support would not work in that manner rather there would still be only one optional argument when returning multiple-values and it would just have a multi-value type, just as the argument can currently be the zero-values type from a nop or a single value type such as i32.
The v8 implementation already appears to be consistent with this change although a small change will be required to sexp-wasm.